@xandreed/plugin-models 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/auth/anthropicOAuth.d.ts +45 -11
- package/dist/auth/anthropicOAuth.js +23 -23
- 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 +52 -0
- package/dist/auth/credential-input.entity.functions.d.ts +18 -0
- package/dist/auth/credential-input.entity.functions.js +12 -0
- package/dist/auth/credential-input.entity.js +22 -0
- package/dist/auth/credential-refresh.entity.d.ts +17 -0
- package/dist/auth/credential-refresh.entity.functions.d.ts +57 -0
- package/dist/auth/credential-refresh.entity.functions.js +21 -0
- package/dist/auth/credential-refresh.entity.js +2 -0
- package/dist/auth/localAuth.d.ts +12 -2
- package/dist/auth/localAuth.js +58 -86
- package/dist/auth/memory-auth.adapter.d.ts +35 -0
- package/dist/auth/memory-auth.adapter.js +35 -0
- package/dist/auth/openAiCodexOAuth.d.ts +28 -6
- package/dist/auth/openAiCodexOAuth.js +22 -32
- 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 +30 -13
- package/dist/index.js +25 -50
- 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 +84 -33
- package/dist/llm/compat.js +188 -121
- 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 +47 -7
- package/dist/llm/modelCatalog.js +31 -57
- package/dist/llm/openAiCodex.d.ts +18 -7
- package/dist/llm/openAiCodex.js +57 -45
- package/dist/llm/openAiCodexWebSocket.d.ts +60 -7
- package/dist/llm/openAiCodexWebSocket.js +113 -105
- 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 +125 -29
- package/dist/llm/providers.js +311 -117
- package/dist/llm/retry.d.ts +11 -47
- package/dist/llm/retry.js +33 -52
- package/dist/llm/router.d.ts +56 -28
- package/dist/llm/router.js +125 -88
- package/dist/llm/sse.d.ts +58 -29
- package/dist/llm/sse.js +47 -39
- 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 +154 -6
- package/dist/plugin.adapter.js +53 -10
- 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 +22 -0
- package/dist/ports/host-credentials.port.js +4 -0
- 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 +66 -19
- package/dist/ports/model-endpoints.port.js +22 -4
- 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 +53 -0
- package/dist/ports/models-transport.port.js +7 -0
- 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 +18 -61
- package/dist/settings/memory-settings.adapter.d.ts +12 -0
- package/dist/settings/memory-settings.adapter.js +11 -0
- 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 +15 -0
- package/dist/settings/settings-value.entity.functions.d.ts +51 -0
- package/dist/settings/settings-value.entity.functions.js +42 -0
- package/dist/settings/settings-value.entity.js +2 -0
- 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 +104 -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.js +42 -0
- 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 +49 -0
- package/dist/transport/models-transport.adapter.js +99 -0
- package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
- package/dist/transport/resilient-http-client.adapter.js +177 -0
- package/dist/transport/undici-fetch.adapter.d.ts +13 -0
- package/dist/transport/undici-fetch.adapter.js +1 -0
- package/package.json +13 -6
- package/dist/auth/anthropicOAuth.test.d.ts +0 -1
- package/dist/auth/localAuth.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/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/roleView.test.d.ts +0 -1
package/dist/llm/openAiCodex.js
CHANGED
|
@@ -1,76 +1,87 @@
|
|
|
1
1
|
import { AiError, LanguageModel } from "effect/ai";
|
|
2
2
|
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
|
|
3
3
|
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
|
-
import { Effect,
|
|
5
|
-
import { CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel,
|
|
6
|
-
import {
|
|
4
|
+
import { Effect, Option, Predicate, Redacted, Schema, Stream } from "effect";
|
|
5
|
+
import { isTurnError, ModelId, ModelSelection, ProviderId, CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, ENGINE_KEY, ModelFailureStamp, modelFailureOf, stampAiErrorFailure } from "@xandreed/core";
|
|
6
|
+
import { foldStreamParts } from "@xandreed/core/internal";
|
|
7
|
+
import { CurrentOpenAiCodexSessions, makeOpenAiCodexSessions, makeOpenAiCodexWebSocketHttpClient } from "./openAiCodexWebSocket.js";
|
|
8
|
+
import { OPENAI_CODEX_ORIGINATOR } from "../auth/openAiCodexOAuth.js";
|
|
9
|
+
import { classifyAiError } from "./provider-failure.entity.functions.js";
|
|
10
|
+
import { withOpenAiFailures } from "./provider-client.adapter.js";
|
|
11
|
+
import { resilientHttpClient, withModelRequestCall, withModelRequestStream } from "../transport/resilient-http-client.adapter.js";
|
|
7
12
|
export const OPENAI_CODEX_API_URL = "https://chatgpt.com/backend-api/codex";
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export const toOpenAiCodexRequestBody = (body, reasoningEffort) => {
|
|
23
|
-
const value = record(body), input = Array.isArray(value.input) ? value.input : [], instructions = input.flatMap(instructionText).join(`
|
|
13
|
+
const ResponsesRequest = Schema.StructWithRest(Schema.Struct({
|
|
14
|
+
input: Schema.optionalKey(Schema.Array(Schema.Json)),
|
|
15
|
+
text: Schema.optionalKey(Schema.Struct({ verbosity: Schema.optionalKey(Schema.Json) })),
|
|
16
|
+
reasoning: Schema.optionalKey(Schema.JsonObject),
|
|
17
|
+
tool_choice: Schema.optionalKey(Schema.Json),
|
|
18
|
+
parallel_tool_calls: Schema.optionalKey(Schema.Json)
|
|
19
|
+
}), [Schema.Record(Schema.String, Schema.Json)]), decodeRequest = Schema.decodeUnknownOption(ResponsesRequest), decodeRequestText = Schema.decodeUnknownOption(Schema.fromJsonString(ResponsesRequest)), InstructionMessage = Schema.Struct({
|
|
20
|
+
role: Schema.Literals(["developer", "system"]),
|
|
21
|
+
content: Schema.Union([Schema.String, Schema.Array(Schema.Json)])
|
|
22
|
+
}), decodeInstruction = Schema.decodeUnknownOption(InstructionMessage), decodeTextPart = Schema.decodeUnknownOption(Schema.Struct({ text: Schema.String })), instructionText = (message) => Option.match(decodeInstruction(message), {
|
|
23
|
+
onNone: () => [],
|
|
24
|
+
onSome: ({ content }) => Predicate.isString(content) ? [content] : content.flatMap((part) => Option.toArray(Option.map(decodeTextPart(part), ({ text }) => text)))
|
|
25
|
+
}), subscriptionBody = (value, reasoningEffort) => {
|
|
26
|
+
const input = value.input ?? [], instructions = input.flatMap(instructionText).join(`
|
|
24
27
|
|
|
25
|
-
`), conversationalInput = input.filter((message) => instructionText(message).length === 0)
|
|
28
|
+
`), conversationalInput = input.filter((message) => instructionText(message).length === 0);
|
|
26
29
|
return {
|
|
27
30
|
...Object.fromEntries(Object.entries(value).filter(([key]) => key !== "max_output_tokens")),
|
|
28
31
|
store: !1,
|
|
29
32
|
stream: !0,
|
|
30
33
|
instructions: instructions.length > 0 ? instructions : "You are a helpful assistant.",
|
|
31
34
|
input: conversationalInput,
|
|
32
|
-
text: { verbosity:
|
|
35
|
+
text: { verbosity: value.text?.verbosity ?? "low" },
|
|
33
36
|
include: ["reasoning.encrypted_content"],
|
|
34
37
|
tool_choice: value.tool_choice ?? "auto",
|
|
35
38
|
parallel_tool_calls: value.parallel_tool_calls ?? !0,
|
|
36
|
-
...
|
|
39
|
+
...Option.match(reasoningEffort, {
|
|
40
|
+
onNone: () => ({}),
|
|
41
|
+
onSome: (effort) => ({ reasoning: { ...value.reasoning, effort, summary: "auto" } })
|
|
42
|
+
})
|
|
37
43
|
};
|
|
38
44
|
};
|
|
45
|
+
export const toOpenAiCodexRequestBody = (body, reasoningEffort) => subscriptionBody(Option.getOrElse(decodeRequest(body), () => ({})), Option.fromNullishOr(reasoningEffort));
|
|
39
46
|
const transformRequest = (accountId, reasoningEffort) => (request) => {
|
|
40
47
|
const withHeaders = request.pipe(HttpClientRequest.setHeaders({
|
|
41
48
|
"chatgpt-account-id": accountId,
|
|
42
|
-
originator:
|
|
49
|
+
originator: OPENAI_CODEX_ORIGINATOR,
|
|
43
50
|
"OpenAI-Beta": "responses=experimental",
|
|
44
51
|
accept: "text/event-stream"
|
|
45
52
|
}));
|
|
46
53
|
if (request.body._tag !== "Uint8Array")
|
|
47
54
|
return withHeaders;
|
|
48
|
-
|
|
49
|
-
return Option.match(Result.getSuccess(Result.try(() => JSON.parse(new TextDecoder().decode(encodedBody)))), {
|
|
55
|
+
return Option.match(decodeRequestText(new TextDecoder().decode(request.body.body)), {
|
|
50
56
|
onNone: () => withHeaders,
|
|
51
|
-
onSome: (decoded) => HttpClientRequest.bodyJsonUnsafe(withHeaders,
|
|
57
|
+
onSome: (decoded) => HttpClientRequest.bodyJsonUnsafe(withHeaders, subscriptionBody(decoded, reasoningEffort))
|
|
52
58
|
});
|
|
53
59
|
}, responseStatus = (error) => AiError.isAiError(error) && ("http" in error.reason) ? error.reason.http?.response?.status : void 0;
|
|
54
|
-
export const mapOpenAiCodexError = (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
reason:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
})
|
|
66
|
-
}) : error, makeOpenAiCodexLanguageModel = (args) => Effect.all({
|
|
60
|
+
export const mapOpenAiCodexError = (_model, error) => {
|
|
61
|
+
if (!AiError.isAiError(error))
|
|
62
|
+
return error;
|
|
63
|
+
const metadata = "metadata" in error.reason ? error.reason.metadata : {}, engine = Object.hasOwn(metadata, ENGINE_KEY) ? metadata[ENGINE_KEY] : void 0;
|
|
64
|
+
if (Predicate.isObject(engine) && Object.hasOwn(engine, "failure") && Option.isSome(Schema.decodeUnknownOption(ModelFailureStamp)(engine)))
|
|
65
|
+
return error;
|
|
66
|
+
const classified = classifyAiError(error, "openai");
|
|
67
|
+
if (Option.exists(modelFailureOf(classified), (kind) => kind !== "Unknown"))
|
|
68
|
+
return classified;
|
|
69
|
+
return responseStatus(classified) === 200 ? stampAiErrorFailure(classified, "InvalidResponse") : classified;
|
|
70
|
+
}, makeOpenAiCodexLanguageModel = (args) => Effect.all({
|
|
67
71
|
cacheKey: Effect.service(CurrentPromptCacheKey),
|
|
68
|
-
policy: Effect.service(CurrentModelCallPolicy)
|
|
69
|
-
|
|
72
|
+
policy: Effect.service(CurrentModelCallPolicy),
|
|
73
|
+
sessions: Effect.flatMap(Effect.service(CurrentOpenAiCodexSessions), Option.match({
|
|
74
|
+
onSome: Effect.succeed,
|
|
75
|
+
onNone: () => makeOpenAiCodexSessions()
|
|
76
|
+
}))
|
|
77
|
+
}, { concurrency: 1 }).pipe(Effect.flatMap(({ cacheKey, policy, sessions }) => OpenAiClient.make({
|
|
70
78
|
apiKey: args.accessToken,
|
|
71
79
|
apiUrl: OPENAI_CODEX_API_URL,
|
|
72
|
-
transformClient: () =>
|
|
73
|
-
|
|
80
|
+
transformClient: () => makeOpenAiCodexWebSocketHttpClient(args.webSocketProxy ?? Option.none(), Option.some(sessions)).pipe(HttpClient.mapRequest((request) => transformRequest(args.accountId, Option.map(policy, (value) => value.effort))(request.pipe(HttpClientRequest.prependUrl(OPENAI_CODEX_API_URL), HttpClientRequest.bearerToken(args.accessToken)))), (client) => resilientHttpClient(client, {
|
|
81
|
+
protocol: "openai",
|
|
82
|
+
adaptResponse: (response) => response
|
|
83
|
+
}))
|
|
84
|
+
}).pipe(Effect.map(withOpenAiFailures), Effect.flatMap((client) => OpenAiLanguageModel.make({
|
|
74
85
|
model: args.model,
|
|
75
86
|
config: {
|
|
76
87
|
strictJsonSchema: !1,
|
|
@@ -89,10 +100,11 @@ export const mapOpenAiCodexError = (model, error) => responseStatus(error) === 4
|
|
|
89
100
|
})
|
|
90
101
|
}
|
|
91
102
|
}).pipe(Effect.provideService(OpenAiClient.OpenAiClient, client))), Effect.map((streaming) => {
|
|
92
|
-
const streamText = (options) => streaming.streamText(options).pipe(Stream.
|
|
103
|
+
const selection = new ModelSelection({ provider: ProviderId.make("openai-codex"), modelId: ModelId.make(args.model) }), streamText = (options) => withModelRequestStream("Stream", selection, streaming.streamText(options)).pipe(Stream.catch((error) => isTurnError(error) ? Stream.die(error) : Stream.fail(mapOpenAiCodexError(args.model, error)))), model = {
|
|
93
104
|
...streaming,
|
|
94
105
|
streamText,
|
|
95
|
-
|
|
106
|
+
generateObject: (options) => withModelRequestCall("Object", selection, streaming.generateObject(options)).pipe(Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(error))),
|
|
107
|
+
generateText: (options) => withModelRequestCall("Text", selection, foldStreamParts(streamText(options), () => Effect.void)).pipe(Effect.map((turn) => new LanguageModel.GenerateTextResponse(turn.content)), Effect.catch((error) => isTurnError(error) ? Effect.die(error) : Effect.fail(error)))
|
|
96
108
|
};
|
|
97
109
|
return describeModel(model, {
|
|
98
110
|
provider: "openai-codex",
|
|
@@ -1,14 +1,67 @@
|
|
|
1
|
-
import { HttpClient } from "effect/http";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare const
|
|
1
|
+
import { HttpClient, HttpClientError } from "effect/http";
|
|
2
|
+
import { Cache, Context, Effect, Option } from "effect";
|
|
3
|
+
import type { WebSocketProxy } from "../ports/models-transport.port.js";
|
|
4
|
+
/** The conversations a client keeps a session id for; past it, the least recently used one gets a fresh id on its next request. */
|
|
5
|
+
export declare const OPENAI_CODEX_SESSION_CAPACITY = 1024;
|
|
6
|
+
/** ChatGPT's Luna router currently keys its internal route on a
|
|
7
|
+
* time-ordered UUIDv7 session id; UUIDv4 reaches a nonexistent rollout
|
|
8
|
+
* alias. Keep one v7 id per Efferent conversation/cache key. `now` is the
|
|
9
|
+
* id's time in epoch milliseconds (`Clock.currentTimeMillis`). */
|
|
10
|
+
export declare const openAiCodexUuidV7: (now: number) => string;
|
|
11
|
+
/** The session id of each conversation (its prompt cache key) a client serves. */
|
|
12
|
+
export type OpenAiCodexSessions = Cache.Cache<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* A session cache: a key's UUIDv7 is minted on its first request and kept
|
|
15
|
+
* while it is used; past `capacity` keys the least recently used is dropped.
|
|
16
|
+
*/
|
|
17
|
+
export declare const makeOpenAiCodexSessions: (capacity?: number) => Effect.Effect<OpenAiCodexSessions>;
|
|
18
|
+
/**
|
|
19
|
+
* The session cache of the routed model a call runs under: each of the
|
|
20
|
+
* models plugin's layers keeps its own, so two agents in one process never
|
|
21
|
+
* share session ids. A subscription model built outside them keeps one of
|
|
22
|
+
* its own.
|
|
23
|
+
*/
|
|
24
|
+
export declare const CurrentOpenAiCodexSessions: Context.Reference<Option.Option<OpenAiCodexSessions>>;
|
|
6
25
|
/** Codex currently names the WebSocket terminal event `response.done`; the
|
|
7
26
|
* public Responses stream consumed by @effect/ai names it `response.completed`. */
|
|
8
27
|
export declare const normalizeOpenAiCodexWebSocketEvent: (value: unknown) => unknown;
|
|
9
28
|
export declare const isOpenAiCodexControlEvent: (type: unknown) => boolean;
|
|
29
|
+
/** The OpenAI error body the HTTP Responses endpoint answers a failed request with. */
|
|
30
|
+
export interface OpenAiErrorBody {
|
|
31
|
+
readonly error: {
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly type: string | null;
|
|
34
|
+
readonly code: string | null;
|
|
35
|
+
readonly param: string | null;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The OpenAI error body of an `error` frame that refuses the request, which
|
|
40
|
+
* the HTTP Responses endpoint would answer with a 400: the frame's status is
|
|
41
|
+
* 400, its error's type or code is `invalid_request_error`, or its message
|
|
42
|
+
* names an image. The error is the frame's `error` object, or the frame
|
|
43
|
+
* itself (the Responses stream's own `error` event). Any other frame: none.
|
|
44
|
+
*/
|
|
45
|
+
export declare const openAiCodexRefusal: (event: unknown) => Option.Option<OpenAiErrorBody>;
|
|
10
46
|
/** Adapt the ChatGPT Codex WebSocket protocol to an SSE-shaped HttpClient
|
|
11
47
|
* response. OpenAiLanguageModel remains the sole protocol decoder, so text,
|
|
12
48
|
* reasoning, tool calls, tool results, usage, and errors keep their standard
|
|
13
|
-
* Effect representations instead of being reimplemented here.
|
|
14
|
-
|
|
49
|
+
* Effect representations instead of being reimplemented here. `proxy` is how
|
|
50
|
+
* the socket reaches ChatGPT (`ModelsTransport.webSocketAgent`); `None`
|
|
51
|
+
* connects directly. A route this runtime cannot use fails every request.
|
|
52
|
+
* `sessions` keeps each conversation's session id (`makeOpenAiCodexSessions`);
|
|
53
|
+
* without it, every request gets a fresh one.
|
|
54
|
+
*
|
|
55
|
+
* Each request opens its own socket (Effect's `Socket`), which closes when
|
|
56
|
+
* the response's stream ends, fails or is cancelled, when the request is
|
|
57
|
+
* aborted, and when the request fails before its first event. The response
|
|
58
|
+
* is the stream once the first event arrives, unless that event is an
|
|
59
|
+
* `error`: the request then fails with the status and the OpenAI error body
|
|
60
|
+
* the HTTP endpoint would answer, so a refused image (`openAiCodexRefusal`)
|
|
61
|
+
* is an `InvalidRequestError` and an exhausted quota a
|
|
62
|
+
* `QuotaExhaustedError`. The stream ends after a terminal event or when the
|
|
63
|
+
* server closes the socket normally; a close before the first event fails
|
|
64
|
+
* the request as a transport error. */
|
|
65
|
+
export declare const makeOpenAiCodexWebSocketHttpClient: (proxy: Option.Option<WebSocketProxy>, sessions?: Option.Option<OpenAiCodexSessions>) => HttpClient.HttpClient.With<HttpClientError.HttpClientError, never>;
|
|
66
|
+
/** The subscription client connecting directly, with no proxy (a fresh session id per request). */
|
|
67
|
+
export declare const OpenAiCodexWebSocketHttpClient: HttpClient.HttpClient.With<HttpClientError.HttpClientError, never>;
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
2
|
-
import {
|
|
2
|
+
import { Socket } from "effect/socket";
|
|
3
|
+
import { Cache, Cause, Clock, Context, Duration, Effect, Exit, Match, Option, Predicate, Pull, Redacted, Result, Schema, Scope, Stream } from "effect";
|
|
3
4
|
import { release } from "node:os";
|
|
4
5
|
import WebSocket from "ws";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}, websocketUrl = (url) => {
|
|
11
|
-
const next = new URL(url);
|
|
12
|
-
next.protocol = next.protocol === "http:" ? "ws:" : "wss:";
|
|
13
|
-
return next.toString();
|
|
14
|
-
};
|
|
15
|
-
export const openAiCodexUuidV7 = (now = Date.now()) => {
|
|
6
|
+
import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
|
|
7
|
+
import { currentRuntime } from "../transport/models-transport.adapter.js";
|
|
8
|
+
import { OPENAI_CODEX_ORIGINATOR } from "../auth/openAiCodexOAuth.js";
|
|
9
|
+
const WEBSOCKET_BETA = "responses_websockets=2026-02-06", OPEN_TIMEOUT = Duration.seconds(30);
|
|
10
|
+
export const OPENAI_CODEX_SESSION_CAPACITY = 1024, openAiCodexUuidV7 = (now) => {
|
|
16
11
|
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)), timestamp = BigInt(now);
|
|
17
12
|
bytes[0] = Number(timestamp >> 40n & 0xffn);
|
|
18
13
|
bytes[1] = Number(timestamp >> 32n & 0xffn);
|
|
@@ -25,98 +20,111 @@ export const openAiCodexUuidV7 = (now = Date.now()) => {
|
|
|
25
20
|
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
26
21
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
27
22
|
};
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
23
|
+
const mintSessionId = Effect.map(Clock.currentTimeMillis, openAiCodexUuidV7);
|
|
24
|
+
export const makeOpenAiCodexSessions = (capacity = OPENAI_CODEX_SESSION_CAPACITY) => Cache.make({ capacity, lookup: (_key) => mintSessionId }), CurrentOpenAiCodexSessions = Context.Reference("@xandreed/plugin-models/CurrentOpenAiCodexSessions", {
|
|
25
|
+
defaultValue: () => Option.none()
|
|
26
|
+
});
|
|
27
|
+
const sessionIdFor = (sessions, cacheKey) => Option.match(Option.all([sessions, cacheKey]), {
|
|
28
|
+
onNone: () => mintSessionId,
|
|
29
|
+
onSome: ([cache, key]) => Cache.get(cache, key)
|
|
30
|
+
}), websocketUrl = (url) => {
|
|
31
|
+
const next = new URL(url);
|
|
32
|
+
next.protocol = next.protocol === "http:" ? "ws:" : "wss:";
|
|
33
|
+
return next.toString();
|
|
34
|
+
}, decodeRequestBody = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.JsonObject)), decodeCacheKey = Schema.decodeUnknownOption(Schema.NonEmptyString), CodexFrame = Schema.StructWithRest(Schema.Struct({ type: Schema.String }), [Schema.Record(Schema.String, Schema.Json)]), decodeFrame = Schema.decodeUnknownResult(Schema.fromJsonString(CodexFrame));
|
|
35
|
+
export const normalizeOpenAiCodexWebSocketEvent = (value) => Predicate.isObject(value) && Predicate.hasProperty(value, "type") && value.type === "response.done" ? { ...value, type: "response.completed" } : value;
|
|
36
|
+
const normalizeFrame = (frame) => frame.type === "response.done" ? { ...frame, type: "response.completed" } : frame, TERMINAL_EVENTS = new Set(["response.completed", "response.incomplete", "response.failed", "error"]);
|
|
37
|
+
export const isOpenAiCodexControlEvent = (type) => typeof type === "string" && (type.startsWith("codex.") || type === "responsesapi.websocket_timing");
|
|
38
|
+
const ErrorDetail = Schema.Struct({
|
|
39
|
+
message: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
40
|
+
type: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
41
|
+
code: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
42
|
+
param: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
43
|
+
}), decodeErrorDetail = Schema.decodeUnknownOption(ErrorDetail), noDetail = {}, ErrorFrame = Schema.Struct({
|
|
44
|
+
type: Schema.Literal("error"),
|
|
45
|
+
status: Schema.optionalKey(Schema.Number),
|
|
46
|
+
error: Schema.optionalKey(Schema.Unknown)
|
|
47
|
+
}), decodeErrorFrame = Schema.decodeUnknownOption(ErrorFrame), ERROR_STATUS = {
|
|
48
|
+
invalid_request_error: 400,
|
|
49
|
+
invalid_api_key_error: 401,
|
|
50
|
+
insufficient_quota_error: 429,
|
|
51
|
+
rate_limit_error: 429,
|
|
52
|
+
service_unavailable_error: 503
|
|
53
|
+
}, statusOfName = (name) => Option.flatMap(name, (key) => Option.fromNullishOr(ERROR_STATUS[key])), errorAnswer = (event) => Option.map(decodeErrorFrame(event), (frame) => {
|
|
54
|
+
const detail = Option.getOrElse(Option.orElse(decodeErrorDetail(frame.error), () => decodeErrorDetail(event)), () => noDetail), message = Option.getOrElse(Option.fromNullishOr(detail.message), () => ""), type = Option.filter(Option.fromNullishOr(detail.type), (value) => value !== "error"), code = Option.fromNullishOr(detail.code), refused = frame.status === 400 || /image/i.test(message) || Option.contains(type, "invalid_request_error") || Option.contains(code, "invalid_request_error");
|
|
55
|
+
return {
|
|
56
|
+
status: refused ? 400 : Option.getOrElse(Option.orElse(Option.filter(Option.fromNullishOr(frame.status), (value) => value >= 400 && value < 600), () => Option.orElse(statusOfName(code), () => statusOfName(type))), () => 500),
|
|
57
|
+
refused,
|
|
58
|
+
body: { error: {
|
|
59
|
+
message: message.length > 0 ? message : refused ? "the request was refused" : "the subscription request failed",
|
|
60
|
+
type: Option.getOrElse(type, () => refused ? "invalid_request_error" : null),
|
|
61
|
+
code: Option.getOrNull(code),
|
|
62
|
+
param: Option.getOrNull(Option.fromNullishOr(detail.param))
|
|
63
|
+
} }
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
export const openAiCodexRefusal = (event) => Option.map(Option.filter(errorAnswer(event), (answer) => answer.refused), (answer) => answer.body);
|
|
67
|
+
const bunSocket = (url, options) => {
|
|
68
|
+
return Reflect.construct(globalThis.WebSocket, [url, options]);
|
|
69
|
+
}, wsSocket = (url, options) => {
|
|
70
|
+
return new WebSocket(url, options);
|
|
71
|
+
}, openSocket = (url, headers, proxy) => Option.match(proxy, {
|
|
72
|
+
onNone: () => wsSocket(url, { headers }),
|
|
73
|
+
onSome: (route) => Match.value(route).pipe(Match.tag("Agent", ({ agent }) => wsSocket(url, { headers, agent })), Match.tag("Proxy", ({ url: proxyUrl }) => bunSocket(url, { headers, proxy: Redacted.value(proxyUrl) })), Match.exhaustive)
|
|
74
|
+
}), routeMismatch = (route) => {
|
|
75
|
+
const runtime = currentRuntime();
|
|
76
|
+
return Match.value(route).pipe(Match.tag("Agent", () => runtime._tag === "Node" ? Option.none() : Option.some(`the WebSocket route is a Node agent, which ${runtime._tag === "Bun" ? "Bun's WebSocket" : "this runtime's WebSocket"} cannot use`)), Match.tag("Proxy", () => runtime._tag === "Bun" ? Option.none() : Option.some("the WebSocket route is a proxy URL for Bun's WebSocket, which this runtime's WebSocket would ignore")), Match.exhaustive, Option.map((why) => `${why}: refused rather than connecting directly past the proxy`));
|
|
77
|
+
}, redacted = (request) => request.pipe(HttpClientRequest.setHeader("authorization", "[redacted]"), HttpClientRequest.setHeader("chatgpt-account-id", "[redacted]")), causeText = (cause) => cause instanceof Error ? cause.message : Predicate.hasProperty(cause, "error") && cause.error instanceof Error ? cause.error.message : Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) && cause.message.length > 0 ? cause.message : Predicate.isObject(cause) ? "the WebSocket handshake failed" : String(cause), socketFailureText = (error) => Match.value(error.reason).pipe(Match.tag("SocketCloseError", (close) => `closed ${close.code}${close.closeReason === void 0 || close.closeReason.length === 0 ? "" : `: ${close.closeReason}`}`), Match.tag("SocketUpgradeError", (upgrade) => upgrade.message), Match.orElse((reason) => reason._tag === "SocketOpenError" && reason.kind === "Timeout" ? reason.message : `${reason.message}: ${causeText(reason.cause)}`)), transportFailure = (request, description, cause) => new HttpClientError.HttpClientError({
|
|
78
|
+
reason: new HttpClientError.TransportError({
|
|
79
|
+
request: redacted(request),
|
|
80
|
+
description: `OpenAI subscription WebSocket failed: ${redactCredentials(description)}`,
|
|
81
|
+
cause
|
|
82
|
+
})
|
|
83
|
+
}), encodeFailure = (request, error) => new HttpClientError.HttpClientError({
|
|
84
|
+
reason: new HttpClientError.EncodeError({ request: redacted(request), description: `the subscription request body is not a JSON object: ${error.message}`, cause: error })
|
|
85
|
+
}), socketHeaders = (request, sessionId) => ({
|
|
86
|
+
...Object.fromEntries(["authorization", "chatgpt-account-id", "originator"].flatMap((key) => Option.toArray(Option.map(Option.fromNullishOr(request.headers[key]), (value) => [key, value])))),
|
|
87
|
+
"openai-beta": WEBSOCKET_BETA,
|
|
88
|
+
"user-agent": `${OPENAI_CODEX_ORIGINATOR} (${process.platform} ${release()}; ${process.arch})`,
|
|
89
|
+
"session-id": sessionId,
|
|
90
|
+
"x-client-request-id": sessionId
|
|
91
|
+
}), decoder = new TextDecoder, encoder = new TextEncoder, isNormalClose = (error) => error.reason._tag === "SocketCloseError" && error.reason.code === 1000, framesOf = (pull) => {
|
|
92
|
+
const next = Effect.catch(pull, (error) => isNormalClose(error) ? Cause.done() : Effect.fail(error));
|
|
93
|
+
return Stream.fromPull(Effect.succeed(next)).pipe(Stream.map((data) => Result.map(decodeFrame(typeof data === "string" ? data : decoder.decode(data)), normalizeFrame)), Stream.filter((frame) => Result.isFailure(frame) || !isOpenAiCodexControlEvent(frame.success.type)), Stream.takeUntil((frame) => Result.isSuccess(frame) && TERMINAL_EVENTS.has(frame.success.type)));
|
|
94
|
+
}, sseBytes = (request) => (frame) => Result.match(frame, {
|
|
95
|
+
onFailure: (error) => Effect.fail(transportFailure(request, `a frame is not a JSON event object: ${error.message}`, error)),
|
|
96
|
+
onSuccess: (event) => Effect.succeed(encoder.encode(`data: ${JSON.stringify(Option.match(errorAnswer(event), {
|
|
97
|
+
onNone: () => event,
|
|
98
|
+
onSome: (answer) => ({ ...event, code: answer.body.error.code ?? answer.body.error.type, message: answer.body.error.message, param: answer.body.error.param })
|
|
99
|
+
}))}
|
|
98
100
|
|
|
99
|
-
`))
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
`))
|
|
102
|
+
}), exchange = (request, url, proxy, sessions, scope) => Effect.gen(function* () {
|
|
103
|
+
const encoded = request.body._tag === "Uint8Array" ? decodeRequestBody(decoder.decode(request.body.body)) : Effect.succeed({}), body = yield* Effect.mapError(encoded, (error) => encodeFailure(request, error)), sessionId = yield* sessionIdFor(sessions, decodeCacheKey(body.prompt_cache_key)), headers = socketHeaders(request, sessionId), socket = yield* Socket.fromWebSocket(Effect.acquireRelease(Effect.try({
|
|
104
|
+
try: () => openSocket(websocketUrl(url), headers, proxy),
|
|
105
|
+
catch: (cause) => new Socket.SocketError({ reason: new Socket.SocketOpenError({ kind: "Unknown", cause }) })
|
|
106
|
+
}), (ws) => Effect.sync(() => ws.close(1000, "done"))), { openTimeout: OPEN_TIMEOUT }), socketFailure = (error) => transportFailure(request, socketFailureText(error), error), { pull } = yield* Scope.provide(socket.reader, scope).pipe(Effect.mapError(socketFailure));
|
|
107
|
+
yield* (yield* Scope.provide(socket.writer, scope)).write(JSON.stringify({ type: "response.create", ...body, prompt_cache_key: sessionId })).pipe(Effect.mapError(socketFailure));
|
|
108
|
+
const events = yield* Scope.provide(Stream.toPull(framesOf(pull)), scope), first = yield* events.pipe(Effect.mapError((error) => Cause.isDone(error) ? error : socketFailure(error)), Pull.catchDone(() => Effect.fail(transportFailure(request, "the socket closed before the first event")))), head = first[0], safeRequest = redacted(request);
|
|
109
|
+
return yield* Option.match(Option.flatMap(Result.getSuccess(head), errorAnswer), {
|
|
110
|
+
onSome: (answer) => Scope.close(scope, Exit.void).pipe(Effect.as(HttpClientResponse.fromWeb(safeRequest, new Response(JSON.stringify(answer.body), {
|
|
111
|
+
status: answer.status,
|
|
112
|
+
headers: { "content-type": "application/json" }
|
|
113
|
+
})))),
|
|
114
|
+
onNone: () => Stream.fromIterable(first).pipe(Stream.concat(Stream.fromPull(Effect.succeed(events))), Stream.mapError(socketFailure), Stream.mapEffect(sseBytes(request)), Stream.tapError((error) => Effect.logWarning(error.message)), Stream.ensuring(Scope.close(scope, Exit.void)), Stream.toReadableStreamEffect(), Effect.map((stream) => HttpClientResponse.fromWeb(safeRequest, new Response(stream, {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: { "content-type": "text/event-stream" }
|
|
117
|
+
}))))
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
export const makeOpenAiCodexWebSocketHttpClient = (proxy, sessions = Option.none()) => HttpClient.make((request, url, signal) => Option.match(Option.flatMap(proxy, routeMismatch), {
|
|
121
|
+
onSome: (why) => Effect.fail(transportFailure(request, why)),
|
|
122
|
+
onNone: () => Effect.gen(function* () {
|
|
123
|
+
const scope = yield* Scope.make(), close = Scope.close(scope, Exit.void), run = Effect.runForkWith(yield* Effect.context()), abort = () => {
|
|
124
|
+
run(close);
|
|
125
|
+
};
|
|
126
|
+
signal.addEventListener("abort", abort, { once: !0 });
|
|
127
|
+
yield* Scope.addFinalizer(scope, Effect.sync(() => signal.removeEventListener("abort", abort)));
|
|
128
|
+
return yield* exchange(request, url, proxy, sessions, scope).pipe(Effect.onExit((exit) => Exit.isSuccess(exit) ? Effect.void : close));
|
|
121
129
|
})
|
|
122
|
-
}));
|
|
130
|
+
})).pipe(HttpClient.filterStatusOk), OpenAiCodexWebSocketHttpClient = makeOpenAiCodexWebSocketHttpClient(Option.none());
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Per-call references identify provisional controls after SDK conversion. Never encoded. */
|
|
3
|
+
export declare const PromptCacheFrame: Schema.Struct<{
|
|
4
|
+
readonly candidates: Schema.$Array<Schema.Struct<{
|
|
5
|
+
readonly control: Schema.Struct<{
|
|
6
|
+
readonly type: Schema.Literal<"ephemeral">;
|
|
7
|
+
readonly ttl: Schema.optionalKey<Schema.Literals<readonly ["5m", "1h"]>>;
|
|
8
|
+
}>;
|
|
9
|
+
readonly priority: Schema.Int;
|
|
10
|
+
}>>;
|
|
11
|
+
readonly policy: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Auto", {
|
|
12
|
+
readonly ttl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
13
|
+
}>, Schema.TaggedStruct<"TopLevel", {
|
|
14
|
+
readonly ttl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
15
|
+
}>, Schema.TaggedStruct<"Off", {}>]>;
|
|
16
|
+
readonly capability: Schema.Boolean;
|
|
17
|
+
}>;
|
|
18
|
+
export type PromptCacheFrame = typeof PromptCacheFrame.Type;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { PromptCacheControl, PromptCacheSettings } from "./prompt-cache.entity.js";
|
|
3
|
+
export const PromptCacheFrame = Schema.Struct({
|
|
4
|
+
candidates: Schema.Array(Schema.Struct({
|
|
5
|
+
control: PromptCacheControl,
|
|
6
|
+
priority: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
7
|
+
})),
|
|
8
|
+
policy: PromptCacheSettings,
|
|
9
|
+
capability: Schema.Boolean
|
|
10
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** The Messages API's supported prompt-cache lifetimes. */
|
|
3
|
+
export declare const PromptCacheTtl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
4
|
+
export type PromptCacheTtl = typeof PromptCacheTtl.Type;
|
|
5
|
+
/** Per-model prompt caching; Off preserves and validates explicit caller controls. @public */
|
|
6
|
+
export declare const PromptCacheSettings: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Auto", {
|
|
7
|
+
readonly ttl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
8
|
+
}>, Schema.TaggedStruct<"TopLevel", {
|
|
9
|
+
readonly ttl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
10
|
+
}>, Schema.TaggedStruct<"Off", {}>]>;
|
|
11
|
+
export type PromptCacheSettings = typeof PromptCacheSettings.Type;
|
|
12
|
+
/** A supported wire control; an omitted lifetime means five minutes. */
|
|
13
|
+
export declare const PromptCacheControl: Schema.Struct<{
|
|
14
|
+
readonly type: Schema.Literal<"ephemeral">;
|
|
15
|
+
readonly ttl: Schema.optionalKey<Schema.Literals<readonly ["5m", "1h"]>>;
|
|
16
|
+
}>;
|
|
17
|
+
export type PromptCacheControl = typeof PromptCacheControl.Type;
|
|
18
|
+
/** One actual cacheable wire position, in semantic cache-prefix order. */
|
|
19
|
+
export declare const CachePosition: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unmarked", {}>, Schema.TaggedStruct<"Caller", {
|
|
20
|
+
readonly control: Schema.Unknown;
|
|
21
|
+
}>, Schema.TaggedStruct<"Auto", {
|
|
22
|
+
readonly priority: Schema.Int;
|
|
23
|
+
}>]>;
|
|
24
|
+
export type CachePosition = typeof CachePosition.Type;
|
|
25
|
+
/** All eligible positions, including unmarked blocks needed to locate the final target. */
|
|
26
|
+
export declare const BreakpointInput: Schema.Struct<{
|
|
27
|
+
readonly positions: Schema.$Array<Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Unmarked", {}>, Schema.TaggedStruct<"Caller", {
|
|
28
|
+
readonly control: Schema.Unknown;
|
|
29
|
+
}>, Schema.TaggedStruct<"Auto", {
|
|
30
|
+
readonly priority: Schema.Int;
|
|
31
|
+
}>]>>;
|
|
32
|
+
readonly policy: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"Auto", {
|
|
33
|
+
readonly ttl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
34
|
+
}>, Schema.TaggedStruct<"TopLevel", {
|
|
35
|
+
readonly ttl: Schema.Literals<readonly ["5m", "1h"]>;
|
|
36
|
+
}>, Schema.TaggedStruct<"Off", {}>]>;
|
|
37
|
+
readonly capability: Schema.Boolean;
|
|
38
|
+
readonly callerTopLevel: Schema.optionalKey<Schema.Unknown>;
|
|
39
|
+
}>;
|
|
40
|
+
export type BreakpointInput = typeof BreakpointInput.Type;
|
|
41
|
+
/** Automatic selections are ascending wire indexes; caller controls remain in place. */
|
|
42
|
+
export declare const BreakpointPlan: Schema.Struct<{
|
|
43
|
+
readonly selectedAuto: Schema.$Array<Schema.Int>;
|
|
44
|
+
readonly topLevel: Schema.OptionFromNullOr<Schema.Struct<{
|
|
45
|
+
readonly type: Schema.Literal<"ephemeral">;
|
|
46
|
+
readonly ttl: Schema.optionalKey<Schema.Literals<readonly ["5m", "1h"]>>;
|
|
47
|
+
}>>;
|
|
48
|
+
}>;
|
|
49
|
+
export type BreakpointPlan = typeof BreakpointPlan.Type;
|
|
50
|
+
declare const PromptCacheError_base: Schema.Class<PromptCacheError, Schema.TaggedStruct<"PromptCacheError", {
|
|
51
|
+
readonly reason: Schema.Literals<readonly ["Control", "Capability", "Capacity", "TtlOrder", "TopLevel"]>;
|
|
52
|
+
readonly message: Schema.String;
|
|
53
|
+
}>, import("effect/Cause").YieldableError>;
|
|
54
|
+
/** Why prepared Messages cache controls cannot form a supported request. */
|
|
55
|
+
export declare class PromptCacheError extends PromptCacheError_base {
|
|
56
|
+
}
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
import { PromptCacheError } from "./prompt-cache.entity.js";
|
|
3
|
+
import type { BreakpointInput, BreakpointPlan } from "./prompt-cache.entity.js";
|
|
4
|
+
/**
|
|
5
|
+
* Plan over actual cacheable wire positions, after SDK conversion. Caller controls are immutable;
|
|
6
|
+
* Auto candidates compete by priority, then later wire position, within four effective slots.
|
|
7
|
+
*/
|
|
8
|
+
export declare const planBreakpoints: (input: BreakpointInput) => Result.Result<BreakpointPlan, PromptCacheError>;
|