@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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/anthropicOAuth.d.ts +45 -11
  3. package/dist/auth/anthropicOAuth.js +23 -23
  4. package/dist/auth/auth-store.conformance.d.ts +63 -0
  5. package/dist/auth/auth-store.conformance.js +152 -0
  6. package/dist/auth/credential-input.entity.d.ts +52 -0
  7. package/dist/auth/credential-input.entity.functions.d.ts +18 -0
  8. package/dist/auth/credential-input.entity.functions.js +12 -0
  9. package/dist/auth/credential-input.entity.js +22 -0
  10. package/dist/auth/credential-refresh.entity.d.ts +17 -0
  11. package/dist/auth/credential-refresh.entity.functions.d.ts +57 -0
  12. package/dist/auth/credential-refresh.entity.functions.js +21 -0
  13. package/dist/auth/credential-refresh.entity.js +2 -0
  14. package/dist/auth/localAuth.d.ts +12 -2
  15. package/dist/auth/localAuth.js +58 -86
  16. package/dist/auth/memory-auth.adapter.d.ts +35 -0
  17. package/dist/auth/memory-auth.adapter.js +35 -0
  18. package/dist/auth/openAiCodexOAuth.d.ts +28 -6
  19. package/dist/auth/openAiCodexOAuth.js +22 -32
  20. package/dist/auth/token-response.entity.d.ts +21 -0
  21. package/dist/auth/token-response.entity.functions.d.ts +20 -0
  22. package/dist/auth/token-response.entity.functions.js +10 -0
  23. package/dist/auth/token-response.entity.js +10 -0
  24. package/dist/compat/index.d.ts +2 -0
  25. package/dist/compat/index.js +1 -0
  26. package/dist/conformance/index.d.ts +6 -0
  27. package/dist/conformance/index.js +3 -0
  28. package/dist/index.d.ts +30 -13
  29. package/dist/index.js +25 -50
  30. package/dist/llm/chat-wire.entity.d.ts +123 -0
  31. package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
  32. package/dist/llm/chat-wire.entity.functions.js +10 -0
  33. package/dist/llm/chat-wire.entity.js +37 -0
  34. package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
  35. package/dist/llm/compat-telemetry.adapter.js +21 -0
  36. package/dist/llm/compat.d.ts +84 -33
  37. package/dist/llm/compat.js +188 -121
  38. package/dist/llm/model-completion.adapter.d.ts +49 -0
  39. package/dist/llm/model-completion.adapter.js +77 -0
  40. package/dist/llm/model-completion.entity.d.ts +4 -0
  41. package/dist/llm/model-completion.entity.functions.d.ts +3 -0
  42. package/dist/llm/model-completion.entity.functions.js +7 -0
  43. package/dist/llm/model-completion.entity.js +7 -0
  44. package/dist/llm/model-retry-policy.entity.d.ts +37 -0
  45. package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
  46. package/dist/llm/model-retry-policy.entity.functions.js +33 -0
  47. package/dist/llm/model-retry-policy.entity.js +23 -0
  48. package/dist/llm/model-settings.entity.d.ts +85 -0
  49. package/dist/llm/model-settings.entity.functions.d.ts +11 -0
  50. package/dist/llm/model-settings.entity.functions.js +49 -0
  51. package/dist/llm/model-settings.entity.js +28 -0
  52. package/dist/llm/model-usage.entity.d.ts +61 -0
  53. package/dist/llm/model-usage.entity.functions.d.ts +11 -0
  54. package/dist/llm/model-usage.entity.functions.js +24 -0
  55. package/dist/llm/model-usage.entity.js +19 -0
  56. package/dist/llm/modelCatalog.d.ts +47 -7
  57. package/dist/llm/modelCatalog.js +31 -57
  58. package/dist/llm/openAiCodex.d.ts +18 -7
  59. package/dist/llm/openAiCodex.js +57 -45
  60. package/dist/llm/openAiCodexWebSocket.d.ts +60 -7
  61. package/dist/llm/openAiCodexWebSocket.js +113 -105
  62. package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
  63. package/dist/llm/prompt-cache-frame.entity.js +10 -0
  64. package/dist/llm/prompt-cache.entity.d.ts +57 -0
  65. package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
  66. package/dist/llm/prompt-cache.entity.functions.js +73 -0
  67. package/dist/llm/prompt-cache.entity.js +27 -0
  68. package/dist/llm/provider-client.adapter.d.ts +9 -0
  69. package/dist/llm/provider-client.adapter.js +60 -0
  70. package/dist/llm/provider-failure.entity.d.ts +42 -0
  71. package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
  72. package/dist/llm/provider-failure.entity.functions.js +67 -0
  73. package/dist/llm/provider-failure.entity.js +20 -0
  74. package/dist/llm/provider-profile.entity.d.ts +79 -0
  75. package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
  76. package/dist/llm/provider-profile.entity.functions.js +83 -0
  77. package/dist/llm/provider-profile.entity.js +10 -0
  78. package/dist/llm/providers.d.ts +125 -29
  79. package/dist/llm/providers.js +311 -117
  80. package/dist/llm/retry.d.ts +11 -47
  81. package/dist/llm/retry.js +33 -52
  82. package/dist/llm/router.d.ts +56 -28
  83. package/dist/llm/router.js +125 -88
  84. package/dist/llm/sse.d.ts +58 -29
  85. package/dist/llm/sse.js +47 -39
  86. package/dist/llm/thinking-binding.entity.d.ts +49 -0
  87. package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
  88. package/dist/llm/thinking-binding.entity.functions.js +16 -0
  89. package/dist/llm/thinking-binding.entity.js +11 -0
  90. package/dist/llm/utilityLlm.d.ts +14 -5
  91. package/dist/llm/utilityLlm.js +18 -13
  92. package/dist/plugin.adapter.d.ts +154 -6
  93. package/dist/plugin.adapter.js +53 -10
  94. package/dist/ports/auth-store.port.d.ts +73 -0
  95. package/dist/ports/auth-store.port.js +24 -0
  96. package/dist/ports/host-credentials.port.d.ts +22 -0
  97. package/dist/ports/host-credentials.port.js +4 -0
  98. package/dist/ports/model-call-completion.port.d.ts +9 -0
  99. package/dist/ports/model-call-completion.port.js +2 -0
  100. package/dist/ports/model-endpoints.port.d.ts +66 -19
  101. package/dist/ports/model-endpoints.port.js +22 -4
  102. package/dist/ports/model-retry-policy.port.d.ts +17 -0
  103. package/dist/ports/model-retry-policy.port.js +3 -0
  104. package/dist/ports/model-settings.port.d.ts +32 -0
  105. package/dist/ports/model-settings.port.js +4 -0
  106. package/dist/ports/models-transport.port.d.ts +53 -0
  107. package/dist/ports/models-transport.port.js +7 -0
  108. package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
  109. package/dist/ports/prompt-cache-frame.port.js +4 -0
  110. package/dist/ports/settings-store.port.d.ts +70 -0
  111. package/dist/ports/settings-store.port.js +33 -0
  112. package/dist/ports/thinking-binding.port.d.ts +7 -0
  113. package/dist/ports/thinking-binding.port.js +4 -0
  114. package/dist/settings/localSettings.d.ts +2 -1
  115. package/dist/settings/localSettings.js +18 -61
  116. package/dist/settings/memory-settings.adapter.d.ts +12 -0
  117. package/dist/settings/memory-settings.adapter.js +11 -0
  118. package/dist/settings/roleView.d.ts +3 -1
  119. package/dist/settings/roleView.js +1 -1
  120. package/dist/settings/settings-store.conformance.d.ts +47 -0
  121. package/dist/settings/settings-store.conformance.js +100 -0
  122. package/dist/settings/settings-value.entity.d.ts +15 -0
  123. package/dist/settings/settings-value.entity.functions.d.ts +51 -0
  124. package/dist/settings/settings-value.entity.functions.js +42 -0
  125. package/dist/settings/settings-value.entity.js +2 -0
  126. package/dist/storage/json-file.d.ts +18 -0
  127. package/dist/storage/json-file.js +26 -0
  128. package/dist/transport/http-proxy.entity.d.ts +104 -0
  129. package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
  130. package/dist/transport/http-proxy.entity.functions.js +112 -0
  131. package/dist/transport/http-proxy.entity.js +42 -0
  132. package/dist/transport/model-request.conformance.d.ts +13 -0
  133. package/dist/transport/model-request.conformance.js +36 -0
  134. package/dist/transport/models-transport.adapter.d.ts +49 -0
  135. package/dist/transport/models-transport.adapter.js +99 -0
  136. package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
  137. package/dist/transport/resilient-http-client.adapter.js +177 -0
  138. package/dist/transport/undici-fetch.adapter.d.ts +13 -0
  139. package/dist/transport/undici-fetch.adapter.js +1 -0
  140. package/package.json +13 -6
  141. package/dist/auth/anthropicOAuth.test.d.ts +0 -1
  142. package/dist/auth/localAuth.test.d.ts +0 -1
  143. package/dist/llm/compat.test.d.ts +0 -1
  144. package/dist/llm/modelCatalog.test.d.ts +0 -1
  145. package/dist/llm/providers.test.d.ts +0 -1
  146. package/dist/llm/retry.test.d.ts +0 -1
  147. package/dist/llm/router.test.d.ts +0 -1
  148. package/dist/llm/sse.test.d.ts +0 -1
  149. package/dist/plugin.adapter.test.d.ts +0 -1
  150. package/dist/settings/localSettings.test.d.ts +0 -1
  151. package/dist/settings/roleView.test.d.ts +0 -1
@@ -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, Result, Option, Redacted, Stream } from "effect";
5
- import { CurrentModelCallPolicy, CurrentPromptCacheKey, describeModel, foldStreamParts } from "@xandreed/core";
6
- import { OpenAiCodexWebSocketHttpClient } from "./openAiCodexWebSocket.js";
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 record = (value) => typeof value === "object" && value !== null ? value : {}, instructionText = (message) => {
9
- const value = record(message);
10
- if (value.role !== "developer" && value.role !== "system")
11
- return [];
12
- const content = value.content;
13
- if (typeof content === "string")
14
- return [content];
15
- if (!Array.isArray(content))
16
- return [];
17
- return content.flatMap((part) => {
18
- const item = record(part);
19
- return typeof item.text === "string" ? [item.text] : [];
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), configuredText = record(value.text);
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: configuredText.verbosity ?? "low" },
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
- ...reasoningEffort === void 0 ? {} : { reasoning: { ...record(value.reasoning), effort: reasoningEffort, summary: "auto" } }
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: "pi",
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
- const encodedBody = request.body.body;
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, toOpenAiCodexRequestBody(decoded, reasoningEffort))
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 = (model, error) => responseStatus(error) === 404 ? AiError.make({
55
- module: "OpenAiCodex",
56
- method: "responses",
57
- reason: new AiError.InvalidRequestError({
58
- description: `model ${model} is not available to the configured ChatGPT subscription account`
59
- })
60
- }) : responseStatus(error) === 200 ? AiError.make({
61
- module: "OpenAiCodex",
62
- method: "responses",
63
- reason: new AiError.InvalidOutputError({
64
- description: `the ${model} subscription stream returned a rejected or incompatible event`
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
- }).pipe(Effect.flatMap(({ cacheKey, policy }) => OpenAiClient.make({
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: () => OpenAiCodexWebSocketHttpClient.pipe(HttpClient.mapRequest((request) => transformRequest(args.accountId, Option.getOrUndefined(Option.map(policy, (value) => value.effort)))(request.pipe(HttpClientRequest.prependUrl(OPENAI_CODEX_API_URL), HttpClientRequest.bearerToken(args.accessToken)))))
73
- }).pipe(Effect.flatMap((client) => OpenAiLanguageModel.make({
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.mapError((error) => mapOpenAiCodexError(args.model, error))), model = {
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
- generateText: (options) => foldStreamParts(streamText(options), () => Effect.void).pipe(Effect.map((turn) => new LanguageModel.GenerateTextResponse(turn.content)))
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
- /** ChatGPT's Luna router currently keys its internal route on Pi's
3
- * time-ordered UUIDv7 session shape; UUIDv4 reaches a nonexistent rollout
4
- * alias. Keep one v7 id per Efferent conversation/cache key. */
5
- export declare const openAiCodexUuidV7: (now?: number) => string;
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
- export declare const OpenAiCodexWebSocketHttpClient: HttpClient.HttpClient;
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 { Effect, Result } from "effect";
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
- const WEBSOCKET_BETA = "responses_websockets=2026-02-06", sessionIds = new Map, decodeBody = (request) => {
6
- if (request.body._tag !== "Uint8Array")
7
- return {};
8
- const text = new TextDecoder().decode(request.body.body), parsed = JSON.parse(text);
9
- return typeof parsed === "object" && parsed !== null ? parsed : {};
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 sessionIdFor = (cacheKey) => {
29
- const key = typeof cacheKey === "string" && cacheKey.length > 0 ? cacheKey : globalThis.crypto.randomUUID(), existing = sessionIds.get(key);
30
- if (existing !== void 0)
31
- return existing;
32
- const created = openAiCodexUuidV7();
33
- sessionIds.set(key, created);
34
- return created;
35
- }, eventText = (data) => {
36
- if (typeof data === "string")
37
- return data;
38
- if (data instanceof ArrayBuffer)
39
- return new TextDecoder().decode(new Uint8Array(data));
40
- if (ArrayBuffer.isView(data))
41
- return new TextDecoder().decode(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
42
- return;
43
- };
44
- export const normalizeOpenAiCodexWebSocketEvent = (value) => {
45
- if (typeof value !== "object" || value === null)
46
- return value;
47
- const event = value;
48
- return event.type === "response.done" ? { ...event, type: "response.completed" } : event;
49
- }, isOpenAiCodexControlEvent = (type) => typeof type === "string" && (type.startsWith("codex.") || type === "responsesapi.websocket_timing"), OpenAiCodexWebSocketHttpClient = HttpClient.make((request, url, signal) => Effect.tryPromise({
50
- try: () => new Promise((resolve, reject) => {
51
- const body = decodeBody(request), safeRequest = request.pipe(HttpClientRequest.setHeader("authorization", "[redacted]"), HttpClientRequest.setHeader("chatgpt-account-id", "[redacted]")), headers = Object.fromEntries(["authorization", "chatgpt-account-id", "originator"].flatMap((key) => {
52
- const value = request.headers[key];
53
- return value === void 0 ? [] : [[key, value]];
54
- }));
55
- headers["openai-beta"] = WEBSOCKET_BETA;
56
- headers["user-agent"] = `pi (${process.platform} ${release()}; ${process.arch})`;
57
- const requestId = sessionIdFor(body.prompt_cache_key);
58
- body.prompt_cache_key = requestId;
59
- headers["session-id"] = requestId;
60
- headers["x-client-request-id"] = requestId;
61
- const socket = new WebSocket(websocketUrl(url), { headers }), state = { settled: !1 }, stream = new ReadableStream({
62
- start: (value) => {
63
- state.controller = value;
64
- },
65
- cancel: () => socket.close(1000, "cancelled")
66
- }), fail = (cause) => {
67
- if (!state.settled) {
68
- state.settled = !0;
69
- reject(cause);
70
- } else
71
- state.controller?.error(cause);
72
- socket.close(1011, "transport error");
73
- }, abort = () => fail(Error("OpenAI subscription request aborted"));
74
- signal.addEventListener("abort", abort, { once: !0 });
75
- socket.addEventListener("open", () => {
76
- Result.match(Result.try(() => {
77
- socket.send(JSON.stringify({ type: "response.create", ...body }));
78
- state.settled = !0;
79
- return HttpClientResponse.fromWeb(safeRequest, new Response(stream, {
80
- status: 200,
81
- headers: { "content-type": "text/event-stream" }
82
- }));
83
- }), { onFailure: fail, onSuccess: resolve });
84
- });
85
- socket.addEventListener("message", (event) => {
86
- Result.match(Result.try(() => {
87
- const text = eventText(event.data);
88
- if (text === void 0)
89
- return;
90
- const parsed = normalizeOpenAiCodexWebSocketEvent(JSON.parse(text)), type = parsed.type;
91
- if (isOpenAiCodexControlEvent(type))
92
- return;
93
- if (type === "error") {
94
- fail(Error(JSON.stringify(parsed)));
95
- return;
96
- }
97
- state.controller?.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(parsed)}
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
- if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
101
- state.controller?.close();
102
- signal.removeEventListener("abort", abort);
103
- socket.close(1000, "done");
104
- }
105
- }), { onFailure: fail, onSuccess: () => {
106
- return;
107
- } });
108
- });
109
- socket.addEventListener("error", (event) => fail(event.error ?? Error(event.message || "WebSocket handshake failed")));
110
- socket.addEventListener("close", (event) => {
111
- if (event.code !== 1000)
112
- fail(Error(`OpenAI subscription WebSocket closed ${event.code}: ${event.reason}`));
113
- });
114
- }),
115
- catch: (cause) => new HttpClientError.HttpClientError({
116
- reason: new HttpClientError.TransportError({
117
- request: request.pipe(HttpClientRequest.setHeader("authorization", "[redacted]"), HttpClientRequest.setHeader("chatgpt-account-id", "[redacted]")),
118
- description: `OpenAI subscription WebSocket failed: ${cause instanceof Error ? cause.message : String(cause)}`,
119
- cause
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>;