@xandreed/plugin-models 0.14.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 +30 -10
  3. package/dist/auth/anthropicOAuth.js +15 -18
  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 +23 -5
  7. package/dist/auth/credential-input.entity.functions.d.ts +15 -5
  8. package/dist/auth/credential-input.entity.functions.js +12 -9
  9. package/dist/auth/credential-input.entity.js +5 -3
  10. package/dist/auth/credential-refresh.entity.d.ts +11 -3
  11. package/dist/auth/credential-refresh.entity.functions.d.ts +48 -6
  12. package/dist/auth/credential-refresh.entity.functions.js +12 -3
  13. package/dist/auth/localAuth.d.ts +10 -1
  14. package/dist/auth/localAuth.js +53 -79
  15. package/dist/auth/memory-auth.adapter.d.ts +12 -4
  16. package/dist/auth/memory-auth.adapter.js +13 -25
  17. package/dist/auth/openAiCodexOAuth.d.ts +26 -5
  18. package/dist/auth/openAiCodexOAuth.js +17 -28
  19. package/dist/auth/token-response.entity.d.ts +21 -0
  20. package/dist/auth/token-response.entity.functions.d.ts +20 -0
  21. package/dist/auth/token-response.entity.functions.js +10 -0
  22. package/dist/auth/token-response.entity.js +10 -0
  23. package/dist/compat/index.d.ts +2 -0
  24. package/dist/compat/index.js +1 -0
  25. package/dist/conformance/index.d.ts +6 -0
  26. package/dist/conformance/index.js +3 -0
  27. package/dist/index.d.ts +19 -19
  28. package/dist/index.js +17 -69
  29. package/dist/llm/chat-wire.entity.d.ts +123 -0
  30. package/dist/llm/chat-wire.entity.functions.d.ts +28 -0
  31. package/dist/llm/chat-wire.entity.functions.js +10 -0
  32. package/dist/llm/chat-wire.entity.js +37 -0
  33. package/dist/llm/compat-telemetry.adapter.d.ts +6 -0
  34. package/dist/llm/compat-telemetry.adapter.js +21 -0
  35. package/dist/llm/compat.d.ts +50 -30
  36. package/dist/llm/compat.js +149 -133
  37. package/dist/llm/model-completion.adapter.d.ts +49 -0
  38. package/dist/llm/model-completion.adapter.js +77 -0
  39. package/dist/llm/model-completion.entity.d.ts +4 -0
  40. package/dist/llm/model-completion.entity.functions.d.ts +3 -0
  41. package/dist/llm/model-completion.entity.functions.js +7 -0
  42. package/dist/llm/model-completion.entity.js +7 -0
  43. package/dist/llm/model-retry-policy.entity.d.ts +37 -0
  44. package/dist/llm/model-retry-policy.entity.functions.d.ts +16 -0
  45. package/dist/llm/model-retry-policy.entity.functions.js +33 -0
  46. package/dist/llm/model-retry-policy.entity.js +23 -0
  47. package/dist/llm/model-settings.entity.d.ts +85 -0
  48. package/dist/llm/model-settings.entity.functions.d.ts +11 -0
  49. package/dist/llm/model-settings.entity.functions.js +49 -0
  50. package/dist/llm/model-settings.entity.js +28 -0
  51. package/dist/llm/model-usage.entity.d.ts +61 -0
  52. package/dist/llm/model-usage.entity.functions.d.ts +11 -0
  53. package/dist/llm/model-usage.entity.functions.js +24 -0
  54. package/dist/llm/model-usage.entity.js +19 -0
  55. package/dist/llm/modelCatalog.d.ts +29 -8
  56. package/dist/llm/modelCatalog.js +28 -66
  57. package/dist/llm/openAiCodex.d.ts +15 -7
  58. package/dist/llm/openAiCodex.js +57 -45
  59. package/dist/llm/openAiCodexWebSocket.d.ts +48 -15
  60. package/dist/llm/openAiCodexWebSocket.js +101 -147
  61. package/dist/llm/prompt-cache-frame.entity.d.ts +18 -0
  62. package/dist/llm/prompt-cache-frame.entity.js +10 -0
  63. package/dist/llm/prompt-cache.entity.d.ts +57 -0
  64. package/dist/llm/prompt-cache.entity.functions.d.ts +8 -0
  65. package/dist/llm/prompt-cache.entity.functions.js +73 -0
  66. package/dist/llm/prompt-cache.entity.js +27 -0
  67. package/dist/llm/provider-client.adapter.d.ts +9 -0
  68. package/dist/llm/provider-client.adapter.js +60 -0
  69. package/dist/llm/provider-failure.entity.d.ts +42 -0
  70. package/dist/llm/provider-failure.entity.functions.d.ts +8 -0
  71. package/dist/llm/provider-failure.entity.functions.js +67 -0
  72. package/dist/llm/provider-failure.entity.js +20 -0
  73. package/dist/llm/provider-profile.entity.d.ts +79 -0
  74. package/dist/llm/provider-profile.entity.functions.d.ts +23 -0
  75. package/dist/llm/provider-profile.entity.functions.js +83 -0
  76. package/dist/llm/provider-profile.entity.js +10 -0
  77. package/dist/llm/providers.d.ts +116 -34
  78. package/dist/llm/providers.js +298 -129
  79. package/dist/llm/retry.d.ts +11 -47
  80. package/dist/llm/retry.js +33 -52
  81. package/dist/llm/router.d.ts +54 -36
  82. package/dist/llm/router.js +91 -78
  83. package/dist/llm/sse.d.ts +44 -37
  84. package/dist/llm/sse.js +43 -35
  85. package/dist/llm/thinking-binding.entity.d.ts +49 -0
  86. package/dist/llm/thinking-binding.entity.functions.d.ts +21 -0
  87. package/dist/llm/thinking-binding.entity.functions.js +16 -0
  88. package/dist/llm/thinking-binding.entity.js +11 -0
  89. package/dist/llm/utilityLlm.d.ts +14 -5
  90. package/dist/llm/utilityLlm.js +18 -13
  91. package/dist/plugin.adapter.d.ts +94 -6
  92. package/dist/plugin.adapter.js +18 -13
  93. package/dist/ports/auth-store.port.d.ts +73 -0
  94. package/dist/ports/auth-store.port.js +24 -0
  95. package/dist/ports/host-credentials.port.d.ts +2 -1
  96. package/dist/ports/model-call-completion.port.d.ts +9 -0
  97. package/dist/ports/model-call-completion.port.js +2 -0
  98. package/dist/ports/model-endpoints.port.d.ts +60 -26
  99. package/dist/ports/model-endpoints.port.js +17 -3
  100. package/dist/ports/model-retry-policy.port.d.ts +17 -0
  101. package/dist/ports/model-retry-policy.port.js +3 -0
  102. package/dist/ports/model-settings.port.d.ts +32 -0
  103. package/dist/ports/model-settings.port.js +4 -0
  104. package/dist/ports/models-transport.port.d.ts +9 -2
  105. package/dist/ports/models-transport.port.js +4 -1
  106. package/dist/ports/prompt-cache-frame.port.d.ts +21 -0
  107. package/dist/ports/prompt-cache-frame.port.js +4 -0
  108. package/dist/ports/settings-store.port.d.ts +70 -0
  109. package/dist/ports/settings-store.port.js +33 -0
  110. package/dist/ports/thinking-binding.port.d.ts +7 -0
  111. package/dist/ports/thinking-binding.port.js +4 -0
  112. package/dist/settings/localSettings.d.ts +2 -1
  113. package/dist/settings/localSettings.js +17 -20
  114. package/dist/settings/memory-settings.adapter.d.ts +3 -1
  115. package/dist/settings/memory-settings.adapter.js +1 -1
  116. package/dist/settings/roleView.d.ts +3 -1
  117. package/dist/settings/roleView.js +1 -1
  118. package/dist/settings/settings-store.conformance.d.ts +47 -0
  119. package/dist/settings/settings-store.conformance.js +100 -0
  120. package/dist/settings/settings-value.entity.d.ts +10 -2
  121. package/dist/settings/settings-value.entity.functions.d.ts +31 -9
  122. package/dist/settings/settings-value.entity.functions.js +5 -6
  123. package/dist/storage/json-file.d.ts +18 -0
  124. package/dist/storage/json-file.js +26 -0
  125. package/dist/transport/http-proxy.entity.d.ts +17 -3
  126. package/dist/transport/model-request.conformance.d.ts +13 -0
  127. package/dist/transport/model-request.conformance.js +36 -0
  128. package/dist/transport/models-transport.adapter.d.ts +18 -17
  129. package/dist/transport/models-transport.adapter.js +28 -31
  130. package/dist/transport/resilient-http-client.adapter.d.ts +73 -0
  131. package/dist/transport/resilient-http-client.adapter.js +177 -0
  132. package/package.json +11 -5
  133. package/dist/auth/anthropicOAuth.test.d.ts +0 -1
  134. package/dist/auth/credential-refresh.entity.functions.test.d.ts +0 -1
  135. package/dist/auth/localAuth.test.d.ts +0 -1
  136. package/dist/auth/memory-auth.adapter.test.d.ts +0 -1
  137. package/dist/llm/compat.test.d.ts +0 -1
  138. package/dist/llm/modelCatalog.test.d.ts +0 -1
  139. package/dist/llm/openAiCodexWebSocket.test.d.ts +0 -1
  140. package/dist/llm/providers.test.d.ts +0 -1
  141. package/dist/llm/retry.test.d.ts +0 -1
  142. package/dist/llm/router.test.d.ts +0 -1
  143. package/dist/llm/sse.test.d.ts +0 -1
  144. package/dist/plugin.adapter.test.d.ts +0 -1
  145. package/dist/settings/localSettings.test.d.ts +0 -1
  146. package/dist/settings/memory-settings.adapter.test.d.ts +0 -1
  147. package/dist/settings/roleView.test.d.ts +0 -1
  148. package/dist/settings/settings-value.entity.functions.test.d.ts +0 -1
  149. package/dist/transport/http-proxy.entity.functions.test.d.ts +0 -1
  150. package/dist/transport/models-transport.adapter.test.d.ts +0 -1
  151. package/dist/transport/recording-proxy.testing.d.ts +0 -33
@@ -1,9 +1,18 @@
1
1
  import { Layer } from "effect";
2
- import { AuthStore } from "@xandreed/core";
2
+ import { AuthStore } from "../ports/auth-store.port.js";
3
3
  export declare const authPaths: (cwd: string, home: string, directory?: string) => ReadonlyArray<string>;
4
4
  /**
5
5
  * `LocalAuthStoreLive(cwd, home)` — read-through: every call re-reads disk,
6
6
  * so a credential added mid-session applies on the next request. A token
7
7
  * refresh goes through the `ModelsTransport` the layer is built over, if any.
8
+ *
9
+ * A file that cannot be read or is not a JSON object fails every call with
10
+ * an `AuthError` of code `auth.corrupt`; only a missing file reads as empty.
11
+ * Each write re-reads its file under that file's lock, which every store in
12
+ * the process shares, and replaces it through a temp file of its own. One
13
+ * refresh of a provider's token runs at a time, and stores its token only if
14
+ * the provider still holds the one it refreshed, so a login or logout made
15
+ * during the refresh wins. The locks hold within one process: two processes
16
+ * sharing these files can still lose an update.
8
17
  */
9
18
  export declare const LocalAuthStoreLive: (cwd: string, home: string, directory?: string, fallbackDirectory?: string) => Layer.Layer<AuthStore, never, never>;
@@ -1,88 +1,62 @@
1
- import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { Clock, Effect, Layer, Option, Redacted } from "effect";
4
- import { asJsonRecord, AuthError, AuthStore, parseJsonWarn } from "@xandreed/core";
5
- import { isUsableAt, refreshCredential } from "./credential-refresh.entity.functions.js";
1
+ import { join } from "node:path";
2
+ import { Effect, Layer, Option, Schema } from "effect";
3
+ import { AuthError, AuthStore, Credential } from "../ports/auth-store.port.js";
4
+ import { encodeCredential } from "./credential-input.entity.functions.js";
5
+ import { isSameToken, resolveSlotKey } from "./credential-refresh.entity.functions.js";
6
6
  import { ModelsTransport } from "../ports/models-transport.port.js";
7
- const decodeCredential = (value) => {
8
- if (typeof value === "string")
9
- return Option.some({ type: "api_key", key: value });
10
- if (typeof value !== "object" || value === null)
11
- return Option.none();
12
- const v = value;
13
- if (v.type === "api_key" && typeof v.key === "string")
14
- return Option.some({ type: "api_key", key: v.key });
15
- if (v.type === "oauth" && typeof v.access === "string" && typeof v.refresh === "string" && typeof v.expires === "number")
16
- return Option.some({
17
- type: "oauth",
18
- access: v.access,
19
- refresh: v.refresh,
20
- expires: v.expires,
21
- ...typeof v.accountId === "string" ? { accountId: v.accountId } : {},
22
- ...typeof v.installationId === "string" ? { installationId: v.installationId } : {}
23
- });
24
- if (v.type === "local")
25
- return Option.some({
26
- type: "local",
27
- ...typeof v.baseUrl === "string" ? { baseUrl: v.baseUrl } : {}
28
- });
29
- return Option.none();
30
- }, readAuthFile = (path) => Effect.tryPromise({ try: () => readFile(path, "utf-8"), catch: () => "missing" }).pipe(Effect.flatMap((text) => parseJsonWarn(text, path)), Effect.map((parsed) => {
31
- const record = asJsonRecord(parsed), entries = new Map(Object.entries(record).flatMap(([provider, raw]) => Option.match(decodeCredential(raw), {
32
- onNone: () => [],
33
- onSome: (cred) => [[provider, cred]]
34
- })));
35
- return { path, entries };
36
- }), Effect.orElseSucceed(() => ({ path, entries: new Map }))), writeAuthFile = (path, entries) => Effect.tryPromise({
37
- try: async () => {
38
- await mkdir(dirname(path), { recursive: !0 });
39
- const tmp = `${path}.tmp-${process.pid}`;
40
- await writeFile(tmp, JSON.stringify(Object.fromEntries(entries), null, 2), "utf-8");
41
- await chmod(tmp, 384);
42
- await rename(tmp, path);
43
- },
44
- catch: (e) => new AuthError({ provider: "*", message: `auth.json write failed: ${String(e)}` })
45
- });
7
+ import { namedLock, readJsonObject, withFileLock, writeJsonObject } from "../storage/json-file.js";
8
+ const decodeStored = Schema.decodeUnknownOption(Credential), decodeLegacyKey = Schema.decodeUnknownOption(Schema.String), decodeCredential = (raw) => Option.orElse(decodeStored(raw), () => Option.flatMap(decodeLegacyKey(raw), (key) => decodeStored({ type: "api_key", key }))), corrupt = (path) => (reason) => new AuthError({
9
+ provider: "*",
10
+ code: "auth.corrupt",
11
+ message: `${path} ${reason}: fix or remove it; nothing was read from it or written to it`
12
+ }), readAuthFile = (path) => readJsonObject(path, corrupt(path)).pipe(Effect.map((record) => {
13
+ const decoded = Object.entries(Option.getOrElse(record, () => ({}))).map(([provider, raw]) => ({ provider, raw, credential: decodeCredential(raw) }));
14
+ return {
15
+ path,
16
+ entries: new Map(decoded.flatMap(({ provider, credential }) => Option.match(credential, {
17
+ onNone: () => [],
18
+ onSome: (found) => [[provider, found]]
19
+ }))),
20
+ unread: new Map(decoded.filter(({ credential }) => Option.isNone(credential)).map(({ provider, raw }) => [provider, raw]))
21
+ };
22
+ })), writeAuthFile = (file, provider, credential) => {
23
+ const others = (entries) => [...entries].filter(([key]) => key !== provider), credentials = [...others(file.entries), ...Option.toArray(Option.map(credential, (value) => [provider, value]))];
24
+ return Effect.forEach(credentials, ([key, value]) => Effect.map(encodeCredential(key, value), (encoded) => [key, encoded]), { concurrency: 1 }).pipe(Effect.flatMap((encoded) => {
25
+ const record = Object.fromEntries([...others(file.unread), ...encoded]);
26
+ return writeJsonObject(file.path, record, Option.some(384), (reason) => new AuthError({ provider: "*", message: `auth.json write failed: ${reason}` }));
27
+ }));
28
+ };
46
29
  export const authPaths = (cwd, home, directory = ".efferent") => [
47
30
  join(home, directory, "auth.json"),
48
31
  join(cwd, directory, "auth.json")
49
- ], LocalAuthStoreLive = (cwd, home, directory = ".efferent", fallbackDirectory) => {
50
- const paths = [...fallbackDirectory === void 0 ? [] : authPaths(cwd, home, fallbackDirectory), ...authPaths(cwd, home, directory)];
32
+ ];
33
+ const holderOf = (files, provider) => Option.firstSomeOf([...files].reverse().map((file) => Option.map(Option.fromNullishOr(file.entries.get(provider)), (credential) => ({ file, credential }))));
34
+ export const LocalAuthStoreLive = (cwd, home, directory = ".efferent", fallbackDirectory) => {
35
+ const writable = authPaths(cwd, home, directory), loginPath = writable[0] ?? join(home, directory, "auth.json"), paths = [...fallbackDirectory === void 0 ? [] : authPaths(cwd, home, fallbackDirectory), ...writable], readAll = Effect.forEach(paths, readAuthFile, { concurrency: 1 }), targetOf = (path) => writable.includes(path) ? path : loginPath;
51
36
  return Layer.effect(AuthStore, Effect.map(Effect.serviceOption(ModelsTransport), (transport) => {
52
- const tokens = Option.match(transport, { onNone: () => ({}), onSome: ({ fetch }) => ({ fetch }) });
37
+ const tokens = Option.match(transport, { onNone: () => ({}), onSome: ({ fetch }) => ({ fetch }) }), slotOf = (provider) => ({
38
+ current: Effect.map(readAll, (files) => Option.map(holderOf(files, provider), ({ credential }) => credential)),
39
+ refreshLock: namedLock(loginPath, `refresh:${provider}`),
40
+ commit: (stale, fresh) => Effect.map(readAll, (files) => holderOf(files, provider)).pipe(Effect.flatMap(Option.match({
41
+ onNone: () => Effect.succeed(!1),
42
+ onSome: ({ file }) => {
43
+ const target = targetOf(file.path);
44
+ return withFileLock(target)(Effect.gen(function* () {
45
+ const now = holderOf(yield* readAll, provider);
46
+ if (!Option.exists(now, (held) => targetOf(held.file.path) === target && isSameToken(held.credential, stale)))
47
+ return !1;
48
+ yield* writeAuthFile(yield* readAuthFile(target), provider, Option.some(fresh));
49
+ return !0;
50
+ }));
51
+ }
52
+ })))
53
+ });
53
54
  return {
54
- all: Effect.gen(function* () {
55
- const files = yield* Effect.forEach(paths, readAuthFile);
56
- return new Map(files.flatMap((f) => [...f.entries]));
57
- }),
58
- get: (provider) => Effect.gen(function* () {
59
- const files = yield* Effect.forEach(paths, readAuthFile), merged = new Map(files.flatMap((f) => [...f.entries]));
60
- return Option.fromNullishOr(merged.get(provider));
61
- }),
62
- resolveKey: (provider) => Effect.gen(function* () {
63
- const holder = [...yield* Effect.forEach(paths, readAuthFile)].reverse().find((f) => f.entries.has(provider)), cred = holder?.entries.get(provider);
64
- if (cred === void 0)
65
- return Option.none();
66
- if (cred.type === "api_key")
67
- return Option.some(Redacted.make(cred.key));
68
- if (cred.type === "local")
69
- return Option.none();
70
- if (isUsableAt(cred, yield* Clock.currentTimeMillis))
71
- return Option.some(Redacted.make(cred.access));
72
- const updated = yield* refreshCredential(provider, cred, tokens), target = holder !== void 0 && authPaths(cwd, home, directory).includes(holder.path) ? holder : yield* readAuthFile(authPaths(cwd, home, directory)[0] ?? "");
73
- yield* writeAuthFile(target.path, new Map([...target.entries, [provider, updated]]));
74
- return Option.some(Redacted.make(updated.access));
75
- }),
76
- set: (provider, credential) => Effect.gen(function* () {
77
- const globalPath = authPaths(cwd, home, directory)[0] ?? "", file = yield* readAuthFile(globalPath);
78
- yield* writeAuthFile(globalPath, new Map([...file.entries, [provider, credential]]));
79
- }),
80
- remove: (provider) => Effect.forEach(paths, (path) => Effect.gen(function* () {
81
- const file = yield* readAuthFile(path);
82
- if (!file.entries.has(provider))
83
- return;
84
- yield* writeAuthFile(path, new Map([...file.entries].filter(([p]) => p !== provider)));
85
- })).pipe(Effect.asVoid)
55
+ all: Effect.map(readAll, (files) => new Map(files.flatMap((f) => [...f.entries]))),
56
+ get: (provider) => slotOf(provider).current,
57
+ resolveKey: (provider) => resolveSlotKey(provider, slotOf(provider), tokens),
58
+ set: (provider, credential) => withFileLock(loginPath)(readAuthFile(loginPath).pipe(Effect.flatMap((file) => writeAuthFile(file, provider, Option.some(credential))))),
59
+ remove: (provider) => readAll.pipe(Effect.andThen(Effect.forEach(paths, (path) => withFileLock(path)(readAuthFile(path).pipe(Effect.flatMap((file) => file.entries.has(provider) || file.unread.has(provider) ? writeAuthFile(file, provider, Option.none()) : Effect.void))), { concurrency: 1, discard: !0 })))
86
60
  };
87
61
  }));
88
62
  };
@@ -1,8 +1,12 @@
1
1
  import { Layer } from "effect";
2
- import { AuthStore } from "@xandreed/core";
3
- import type { AuthError, Credential } from "@xandreed/core";
2
+ import { AuthStore } from "../ports/auth-store.port.js";
3
+ import type { AuthError, Credential } from "../ports/auth-store.port.js";
4
4
  import type { ModelsFetch } from "../ports/models-transport.port.js";
5
- /** How the in-memory store reaches the OAuth token endpoints. */
5
+ /**
6
+ * How the in-memory store reaches the OAuth token endpoints.
7
+ *
8
+ * @public
9
+ */
6
10
  export interface MemoryAuthOptions {
7
11
  /** The fetch token refreshes go through; default: the `ModelsTransport`'s when the layer is built over one, else the global fetch. */
8
12
  readonly fetch?: ModelsFetch;
@@ -14,7 +18,9 @@ export interface MemoryAuthOptions {
14
18
  * `seed`. `resolveKey` refreshes a near-expiry anthropic or openai-codex
15
19
  * OAuth token in memory, one refresh per provider at a time (through the
16
20
  * `ModelsTransport` it was built over, so through the plugin's proxy), and
17
- * hands the new token to `HostCredentials.save`.
21
+ * hands the new token to `HostCredentials.save`. Secrets stay `Redacted`
22
+ * throughout; a `set` whose secrets are not (a plain string from untyped
23
+ * code) fails with an `AuthError`, as it does on the file store.
18
24
  *
19
25
  * A login or logout (`set`, `remove`) does not wait for a refresh's token
20
26
  * request; it waits only while a refreshed token is committed, which
@@ -23,5 +29,7 @@ export interface MemoryAuthOptions {
23
29
  * credential it refreshed (compare-and-set under the provider's lock): a
24
30
  * logout during a stalled token request stays a logout, and the host never
25
31
  * receives the refreshed token.
32
+ *
33
+ * @public
26
34
  */
27
35
  export declare const MemoryAuthStoreLive: (seed?: ReadonlyMap<string, Credential>, options?: MemoryAuthOptions) => Layer.Layer<AuthStore, AuthError>;
@@ -1,47 +1,35 @@
1
- import { Clock, Effect, Layer, Match, Option, Redacted, Ref, Semaphore, SynchronizedRef } from "effect";
2
- import { AuthStore } from "@xandreed/core";
1
+ import { Effect, Layer, Option, Ref, Semaphore, SynchronizedRef } from "effect";
2
+ import { AuthStore } from "../ports/auth-store.port.js";
3
3
  import { HostCredentials } from "../ports/host-credentials.port.js";
4
4
  import { ModelsTransport } from "../ports/models-transport.port.js";
5
- import { toCredential, toCredentialInput } from "./credential-input.entity.functions.js";
6
- import { isUsableAt, refreshCredential } from "./credential-refresh.entity.functions.js";
5
+ import { encodeCredential, toCredentialInput } from "./credential-input.entity.functions.js";
6
+ import { isSameToken, resolveSlotKey } from "./credential-refresh.entity.functions.js";
7
7
  export const MemoryAuthStoreLive = (seed = new Map, options = {}) => Layer.effect(AuthStore, Effect.gen(function* () {
8
8
  const host = yield* Effect.serviceOption(HostCredentials), loaded = yield* Option.match(host, {
9
9
  onNone: () => Effect.succeed(new Map),
10
- onSome: (credentials) => credentials.load.pipe(Effect.map((all) => new Map([...all].map(([provider, input]) => [provider, toCredential(input)]))))
10
+ onSome: (credentials) => credentials.load
11
11
  }), state = yield* Ref.make(new Map([...loaded, ...seed])), locks = yield* SynchronizedRef.make(new Map), transport = yield* Effect.serviceOption(ModelsTransport), tokens = Option.match(Option.orElse(Option.fromNullishOr(options.fetch), () => Option.map(transport, (found) => found.fetch)), {
12
12
  onNone: () => ({}),
13
13
  onSome: (fetch) => ({ fetch })
14
14
  }), locksOf = (provider) => SynchronizedRef.modifyEffect(locks, (all) => Option.match(Option.fromNullishOr(all.get(provider)), {
15
15
  onSome: (found) => Effect.succeed([found, all]),
16
- onNone: () => Effect.all({ commit: Semaphore.make(1), refresh: Semaphore.make(1) }).pipe(Effect.map((made) => [made, new Map([...all, [provider, made]])]))
16
+ onNone: () => Effect.all({ commit: Semaphore.make(1), refresh: Semaphore.make(1) }, { concurrency: 1 }).pipe(Effect.map((made) => [made, new Map([...all, [provider, made]])]))
17
17
  })), current = (provider) => Ref.get(state).pipe(Effect.map((all) => Option.fromNullishOr(all.get(provider)))), save = (provider, credential) => Option.match(host, {
18
18
  onNone: () => Effect.void,
19
19
  onSome: (credentials) => credentials.save(provider, Option.map(credential, toCredentialInput))
20
20
  }), write = (provider, credential) => Ref.update(state, (all) => Option.match(credential, {
21
21
  onNone: () => new Map([...all].filter(([key]) => key !== provider)),
22
22
  onSome: (value) => new Map([...all, [provider, value]])
23
- })), refreshing = (provider, stale) => locksOf(provider).pipe(Effect.flatMap((lock) => lock.refresh.withPermit(Effect.gen(function* () {
24
- const before = yield* current(provider);
25
- if (!Option.exists(before, (held) => held === stale))
26
- return { _tag: "Changed" };
27
- const fresh = yield* refreshCredential(provider, stale, tokens);
28
- return yield* lock.commit.withPermit(Effect.gen(function* () {
29
- const now = yield* current(provider);
30
- if (!Option.exists(now, (held) => held === stale))
31
- return { _tag: "Changed" };
32
- yield* write(provider, Option.some(fresh));
33
- yield* save(provider, Option.some(fresh)).pipe(Effect.catch((error) => Effect.logWarning(`the refreshed ${provider} token is kept in memory, but the host did not save it: ${error.message}`)));
34
- return { _tag: "Key", key: Redacted.make(fresh.access) };
35
- }));
36
- }))), Effect.flatMap((refreshed) => Match.value(refreshed).pipe(Match.tag("Key", ({ key }) => Effect.succeed(Option.some(key))), Match.tag("Changed", () => resolveKey(provider)), Match.exhaustive))), resolveKey = (provider) => current(provider).pipe(Effect.flatMap(Option.match({
37
- onNone: () => Effect.succeed(Option.none()),
38
- onSome: (credential) => Match.value(credential).pipe(Match.when({ type: "api_key" }, ({ key }) => Effect.succeed(Option.some(Redacted.make(key)))), Match.when({ type: "local" }, () => Effect.succeed(Option.none())), Match.when({ type: "oauth" }, (oauth) => Clock.currentTimeMillis.pipe(Effect.flatMap((now) => isUsableAt(oauth, now) ? Effect.succeed(Option.some(Redacted.make(oauth.access))) : refreshing(provider, oauth)))), Match.exhaustive)
39
- })));
23
+ })), slotOf = (provider) => ({
24
+ current: current(provider),
25
+ refreshLock: Effect.map(locksOf(provider), (lock) => lock.refresh),
26
+ commit: (stale, fresh) => locksOf(provider).pipe(Effect.flatMap((lock) => lock.commit.withPermit(current(provider).pipe(Effect.flatMap((now) => !Option.exists(now, (held) => isSameToken(held, stale)) ? Effect.succeed(!1) : write(provider, Option.some(fresh)).pipe(Effect.andThen(save(provider, Option.some(fresh)).pipe(Effect.catch((error) => Effect.logWarning(`the refreshed ${provider} token is kept in memory, but the host did not save it: ${error.message}`)))), Effect.as(!0)))))))
27
+ });
40
28
  return AuthStore.of({
41
29
  all: Ref.get(state),
42
30
  get: current,
43
- resolveKey,
44
- set: (provider, credential) => locksOf(provider).pipe(Effect.flatMap((lock) => lock.commit.withPermit(save(provider, Option.some(credential)).pipe(Effect.andThen(write(provider, Option.some(credential))))))),
31
+ resolveKey: (provider) => resolveSlotKey(provider, slotOf(provider), tokens),
32
+ set: (provider, credential) => encodeCredential(provider, credential).pipe(Effect.andThen(locksOf(provider)), Effect.flatMap((lock) => lock.commit.withPermit(save(provider, Option.some(credential)).pipe(Effect.andThen(write(provider, Option.some(credential))))))),
45
33
  remove: (provider) => locksOf(provider).pipe(Effect.flatMap((lock) => lock.commit.withPermit(current(provider).pipe(Effect.flatMap((held) => Option.isNone(held) ? Effect.void : save(provider, Option.none()).pipe(Effect.andThen(write(provider, Option.none()))))))))
46
34
  });
47
35
  }));
@@ -1,17 +1,27 @@
1
- import { Effect, Option } from "effect";
2
- import { AuthError } from "@xandreed/core";
1
+ import { Effect, Option, Redacted } from "effect";
2
+ import { AuthError } from "../ports/auth-store.port.js";
3
3
  import type { TokenRequestOptions } from "./anthropicOAuth.js";
4
4
  /** ChatGPT Plus/Pro OAuth protocol used by the OpenAI Codex subscription. */
5
5
  export declare const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
6
+ /** The originator registered with `OPENAI_CODEX_CLIENT_ID`. The authorize
7
+ * request, the token and the subscription transports must all send it: the
8
+ * backend keys model entitlement to the client/originator pair. */
9
+ export declare const OPENAI_CODEX_ORIGINATOR = "pi";
6
10
  export declare const OPENAI_CODEX_CALLBACK_PORT = 1455;
7
11
  export declare const OPENAI_CODEX_CALLBACK_PATH = "/auth/callback";
8
12
  export declare const OPENAI_CODEX_REDIRECT_URI = "http://localhost:1455/auth/callback";
13
+ /**
14
+ * The tokens a sign-in or a refresh grants, `Redacted` from the token response on, and the account id read from the access token.
15
+ *
16
+ * @public
17
+ */
9
18
  export interface OpenAiCodexTokens {
10
- readonly access: string;
11
- readonly refresh: string;
19
+ readonly access: Redacted.Redacted<string>;
20
+ readonly refresh: Redacted.Redacted<string>;
12
21
  readonly expires: number;
13
22
  readonly accountId?: string;
14
23
  }
24
+ /** @public */
15
25
  export interface OpenAiCodexOAuthBegin {
16
26
  readonly verifier: string;
17
27
  readonly state: string;
@@ -21,6 +31,17 @@ export interface OpenAiCodexOAuthBegin {
21
31
  }
22
32
  /** Extract the subscription account lane without exposing the token. */
23
33
  export declare const openAiCodexAccountId: (accessToken: string) => Option.Option<string>;
34
+ /** @public */
24
35
  export declare const beginOpenAiCodexOAuth: Effect.Effect<OpenAiCodexOAuthBegin>;
36
+ /**
37
+ * Exchange the authorization code for tokens, `Redacted` (ready for `AuthStore.set`).
38
+ *
39
+ * @public
40
+ */
25
41
  export declare const exchangeOpenAiCodexCode: (code: string, verifier: string, options?: TokenRequestOptions) => Effect.Effect<OpenAiCodexTokens, AuthError>;
26
- export declare const refreshOpenAiCodexToken: (refreshToken: string, options?: TokenRequestOptions) => Effect.Effect<OpenAiCodexTokens, AuthError>;
42
+ /**
43
+ * Refresh an expired (or near-expiry) access token; the refresh token is unwrapped only into the request form.
44
+ *
45
+ * @public
46
+ */
47
+ export declare const refreshOpenAiCodexToken: (refreshToken: Redacted.Redacted<string>, options?: TokenRequestOptions) => Effect.Effect<OpenAiCodexTokens, AuthError>;
@@ -1,25 +1,17 @@
1
- import { Effect, Result, Option } from "effect";
2
- import { AuthError } from "@xandreed/core";
1
+ import { Effect, Result, Option, Redacted, Schema } from "effect";
2
+ import { Base64Url } from "effect/encoding";
3
+ import { AuthError } from "../ports/auth-store.port.js";
3
4
  import { generatePkce, tokenTime } from "./anthropicOAuth.js";
4
5
  import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
5
- export const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
6
+ import { decodeTokenResponse, tokenRefusalText } from "./token-response.entity.functions.js";
7
+ export const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", OPENAI_CODEX_ORIGINATOR = "pi";
6
8
  const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize", TOKEN_URL = "https://auth.openai.com/oauth/token";
7
9
  export const OPENAI_CODEX_CALLBACK_PORT = 1455, OPENAI_CODEX_CALLBACK_PATH = "/auth/callback", OPENAI_CODEX_REDIRECT_URI = `http://localhost:${OPENAI_CODEX_CALLBACK_PORT}${OPENAI_CODEX_CALLBACK_PATH}`;
8
- const SCOPE = "openid profile email offline_access", ACCOUNT_CLAIM = "https://api.openai.com/auth", oauthError = (stage, detail) => new AuthError({ provider: "openai-codex", message: `OAuth ${stage} failed: ${detail}` }), base64UrlJson = (segment) => {
9
- const normalized = segment.replace(/-/g, "+").replace(/_/g, "/"), padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
10
- return JSON.parse(atob(padded));
11
- };
12
- export const openAiCodexAccountId = (accessToken) => {
13
- const segment = accessToken.split(".")[1];
14
- return Option.flatMap(Option.fromNullishOr(segment), (encoded) => Option.flatMap(Result.getSuccess(Result.try(() => base64UrlJson(encoded))), (payload) => {
15
- const claim = payload[ACCOUNT_CLAIM];
16
- if (typeof claim !== "object" || claim === null)
17
- return Option.none();
18
- const accountId = claim.chatgpt_account_id;
19
- return Option.filter(Option.fromNullishOr(typeof accountId === "string" ? accountId : void 0), (value) => value.length > 0);
20
- }));
21
- };
22
- const postToken = (body, stage, options) => Effect.tryPromise({
10
+ const SCOPE = "openid profile email offline_access", ACCOUNT_CLAIM = "https://api.openai.com/auth", oauthError = (stage, detail) => new AuthError({ provider: "openai-codex", message: `OAuth ${stage} failed: ${detail}` }), AccessTokenClaims = Schema.Struct({
11
+ [ACCOUNT_CLAIM]: Schema.optionalKey(Schema.Struct({ chatgpt_account_id: Schema.optionalKey(Schema.NonEmptyString) }))
12
+ }), decodeClaims = Schema.decodeUnknownOption(Schema.fromJsonString(AccessTokenClaims)), claimsOf = (segment) => Option.flatMap(Result.getSuccess(Base64Url.decodeString(segment)), decodeClaims);
13
+ export const openAiCodexAccountId = (accessToken) => Option.flatMap(Option.flatMap(Option.fromNullishOr(accessToken.split(".")[1]), claimsOf), (claims) => Option.fromNullishOr(claims[ACCOUNT_CLAIM]?.chatgpt_account_id));
14
+ const postToken = (body, stage, options, previous) => Effect.tryPromise({
23
15
  try: async () => {
24
16
  const response = await (options.fetch ?? fetch)(TOKEN_URL, {
25
17
  method: "POST",
@@ -30,14 +22,11 @@ const postToken = (body, stage, options) => Effect.tryPromise({
30
22
  return { ok: response.ok, status: response.status, text: await response.text() };
31
23
  },
32
24
  catch: (error) => oauthError(stage, redactCredentials(String(error)))
33
- }).pipe(Effect.filterOrFail((response) => response.ok, (response) => oauthError(stage, `status ${response.status}: ${response.text.slice(0, 200)}`)), Effect.flatMap((response) => Effect.try({
34
- try: () => JSON.parse(response.text),
35
- catch: () => oauthError(stage, "the token response was not JSON")
36
- })), Effect.filterOrFail((token) => typeof token.access_token === "string" && typeof token.refresh_token === "string" && typeof token.expires_in === "number", () => oauthError(stage, "the token response omitted required fields")), Effect.flatMap((token) => Effect.map(tokenTime(options), (now) => ({
37
- access: token.access_token,
38
- refresh: token.refresh_token,
39
- expires: now + token.expires_in * 1000,
40
- ...Option.match(openAiCodexAccountId(token.access_token), { onNone: () => ({}), onSome: (accountId) => ({ accountId }) })
25
+ }).pipe(Effect.filterOrFail((response) => response.ok, (response) => oauthError(stage, tokenRefusalText(response.status, response.text, body))), Effect.flatMap((response) => decodeTokenResponse(response.text, previous).pipe(Effect.mapError((reason) => oauthError(stage, reason)))), Effect.flatMap((issued) => Effect.map(tokenTime(options), (now) => ({
26
+ access: issued.access,
27
+ refresh: issued.refresh,
28
+ expires: now + issued.lifetimeMs,
29
+ ...Option.match(openAiCodexAccountId(Redacted.value(issued.access)), { onNone: () => ({}), onSome: (accountId) => ({ accountId }) })
41
30
  }))));
42
31
  export const beginOpenAiCodexOAuth = Effect.gen(function* () {
43
32
  const pkce = yield* generatePkce, stateBytes = new Uint8Array(16);
@@ -53,7 +42,7 @@ export const beginOpenAiCodexOAuth = Effect.gen(function* () {
53
42
  state,
54
43
  id_token_add_organizations: "true",
55
44
  codex_cli_simplified_flow: "true",
56
- originator: "pi"
45
+ originator: OPENAI_CODEX_ORIGINATOR
57
46
  }).forEach(([key, value]) => url.searchParams.set(key, value));
58
47
  return { verifier: pkce.verifier, state, authorizeUrl: url.toString(), callbackPort: OPENAI_CODEX_CALLBACK_PORT, callbackPath: OPENAI_CODEX_CALLBACK_PATH };
59
- }), exchangeOpenAiCodexCode = (code, verifier, options = {}) => postToken({ grant_type: "authorization_code", client_id: OPENAI_CODEX_CLIENT_ID, code, code_verifier: verifier, redirect_uri: OPENAI_CODEX_REDIRECT_URI }, "code exchange", options), refreshOpenAiCodexToken = (refreshToken, options = {}) => postToken({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: OPENAI_CODEX_CLIENT_ID }, "token refresh", options);
48
+ }), exchangeOpenAiCodexCode = (code, verifier, options = {}) => postToken({ grant_type: "authorization_code", client_id: OPENAI_CODEX_CLIENT_ID, code, code_verifier: verifier, redirect_uri: OPENAI_CODEX_REDIRECT_URI }, "code exchange", options, Option.none()), refreshOpenAiCodexToken = (refreshToken, options = {}) => postToken({ grant_type: "refresh_token", refresh_token: Redacted.value(refreshToken), client_id: OPENAI_CODEX_CLIENT_ID }, "token refresh", options, Option.some(refreshToken));
@@ -0,0 +1,21 @@
1
+ import { Schema } from "effect";
2
+ /**
3
+ * A token endpoint's answer, as both subscription providers send it: the
4
+ * access token, its lifetime in seconds, and a refresh token. A refresh may
5
+ * leave the refresh token out, when the provider does not rotate it. The
6
+ * tokens decode `Redacted`, so from the response on they print as
7
+ * `<redacted>` in errors, logs and fingerprints.
8
+ */
9
+ export declare const TokenResponse: Schema.Struct<{
10
+ readonly access_token: Schema.RedactedFromValue<Schema.NonEmptyString>;
11
+ readonly expires_in: Schema.Finite;
12
+ readonly refresh_token: Schema.OptionFromOptional<Schema.RedactedFromValue<Schema.String>>;
13
+ }>;
14
+ export type TokenResponse = typeof TokenResponse.Type;
15
+ /** The tokens a response grants, `Redacted`: the access token, the refresh token to keep, and the access token's lifetime in ms. */
16
+ export declare const IssuedTokens: Schema.Struct<{
17
+ readonly access: Schema.Redacted<Schema.String>;
18
+ readonly refresh: Schema.Redacted<Schema.String>;
19
+ readonly lifetimeMs: Schema.Number;
20
+ }>;
21
+ export type IssuedTokens = typeof IssuedTokens.Type;
@@ -0,0 +1,20 @@
1
+ import { Effect, Option, Redacted } from "effect";
2
+ import type { IssuedTokens } from "./token-response.entity.js";
3
+ /** How much of a refused token request's response body an error quotes. */
4
+ export declare const TOKEN_ERROR_BODY_CHARS = 200;
5
+ /**
6
+ * What an error says of a token endpoint's refusal: its status and the
7
+ * start of its body, every secret the request sent (`SECRET_FIELDS` of
8
+ * `sent`) replaced by `<redacted>` first, so an endpoint that echoes the
9
+ * request never puts a token into an error, a log or the model's view.
10
+ */
11
+ export declare const tokenRefusalText: (status: number, body: string, sent: Readonly<Record<string, string>>) => string;
12
+ /**
13
+ * Decode a token endpoint's answer, its tokens `Redacted`. `previous` is
14
+ * the refresh token a refresh sent: a response without one keeps it. A code
15
+ * exchange sends none, so its response must carry a refresh token. A
16
+ * response that is not JSON, has no access token or no lifetime above zero
17
+ * seconds fails with a reason that never quotes the response, which holds
18
+ * secrets.
19
+ */
20
+ export declare const decodeTokenResponse: (text: string, previous: Option.Option<Redacted.Redacted<string>>) => Effect.Effect<IssuedTokens, string>;
@@ -0,0 +1,10 @@
1
+ import { Effect, Option, Redacted, Schema } from "effect";
2
+ import { TokenResponse } from "./token-response.entity.js";
3
+ const decodeResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(TokenResponse)), isNonEmpty = (token) => Redacted.value(token).length > 0, SECRET_FIELDS = ["refresh_token", "code", "code_verifier"];
4
+ export const TOKEN_ERROR_BODY_CHARS = 200, tokenRefusalText = (status, body, sent) => {
5
+ const redacted = SECRET_FIELDS.flatMap((field) => Option.toArray(Option.filter(Option.fromNullishOr(sent[field]), (value) => value.length > 0))).reduce((text, secret) => text.replaceAll(secret, "<redacted>"), body);
6
+ return `status ${status}: ${redacted.slice(0, TOKEN_ERROR_BODY_CHARS)}`;
7
+ }, decodeTokenResponse = (text, previous) => decodeResponse(text).pipe(Effect.mapError(() => "the token response did not hold an access token and its lifetime (expires_in, in seconds, above 0)"), Effect.flatMap((response) => Option.match(Option.orElse(Option.filter(response.refresh_token, isNonEmpty), () => previous), {
8
+ onNone: () => Effect.fail("the token response did not hold a refresh token"),
9
+ onSome: (refresh) => Effect.succeed({ access: response.access_token, refresh, lifetimeMs: response.expires_in * 1000 })
10
+ })));
@@ -0,0 +1,10 @@
1
+ import { Schema } from "effect";
2
+ export const TokenResponse = Schema.Struct({
3
+ access_token: Schema.RedactedFromValue(Schema.NonEmptyString),
4
+ expires_in: Schema.Finite.check(Schema.isGreaterThan(0)),
5
+ refresh_token: Schema.OptionFromOptional(Schema.RedactedFromValue(Schema.String))
6
+ }), IssuedTokens = Schema.Struct({
7
+ access: Schema.Redacted(Schema.String),
8
+ refresh: Schema.Redacted(Schema.String),
9
+ lifetimeMs: Schema.Number
10
+ });
@@ -0,0 +1,2 @@
1
+ export { makeCompatLanguageModel, promptWithImageLabels, toChatMessages } from "../llm/compat.js";
2
+ export type { ChatMessageOptions, CompatConfig } from "../llm/compat.js";
@@ -0,0 +1 @@
1
+ export { makeCompatLanguageModel, promptWithImageLabels, toChatMessages } from "../llm/compat.js";
@@ -0,0 +1,6 @@
1
+ export { authStoreConformance } from "../auth/auth-store.conformance.js";
2
+ export type { AuthStoreConformanceOptions, AuthStoreRefreshProbe } from "../auth/auth-store.conformance.js";
3
+ export { settingsStoreConformance } from "../settings/settings-store.conformance.js";
4
+ export type { SettingsStoreConformanceOptions } from "../settings/settings-store.conformance.js";
5
+ export { modelRequestConformance } from "../transport/model-request.conformance.js";
6
+ export type { ModelRequestSubject } from "../transport/model-request.conformance.js";
@@ -0,0 +1,3 @@
1
+ export { authStoreConformance } from "../auth/auth-store.conformance.js";
2
+ export { settingsStoreConformance } from "../settings/settings-store.conformance.js";
3
+ export { modelRequestConformance } from "../transport/model-request.conformance.js";
package/dist/index.d.ts CHANGED
@@ -1,37 +1,37 @@
1
- export { ANTHROPIC_OAUTH_BETA, beginAnthropicOAuth, CLAUDE_CODE_SYSTEM, exchangeAnthropicCode, generatePkce, parseAuthorizationInput, refreshAnthropicToken, } from "./auth/anthropicOAuth.js";
2
- export type { AuthRedirect, OAuthBegin, Pkce, RefreshedTokens, TokenRequestOptions } from "./auth/anthropicOAuth.js";
3
- export { beginOpenAiCodexOAuth, exchangeOpenAiCodexCode, OPENAI_CODEX_CALLBACK_PATH, OPENAI_CODEX_CALLBACK_PORT, OPENAI_CODEX_CLIENT_ID, OPENAI_CODEX_REDIRECT_URI, openAiCodexAccountId, refreshOpenAiCodexToken, } from "./auth/openAiCodexOAuth.js";
1
+ export { beginAnthropicOAuth, exchangeAnthropicCode, parseAuthorizationInput, refreshAnthropicToken } from "./auth/anthropicOAuth.js";
2
+ export type { AuthRedirect, OAuthBegin, RefreshedTokens, TokenRequestOptions } from "./auth/anthropicOAuth.js";
3
+ export { beginOpenAiCodexOAuth, exchangeOpenAiCodexCode, refreshOpenAiCodexToken } from "./auth/openAiCodexOAuth.js";
4
4
  export type { OpenAiCodexOAuthBegin, OpenAiCodexTokens } from "./auth/openAiCodexOAuth.js";
5
- export { authPaths, LocalAuthStoreLive } from "./auth/localAuth.js";
5
+ export { LocalAuthStoreLive } from "./auth/localAuth.js";
6
6
  export { MemoryAuthStoreLive } from "./auth/memory-auth.adapter.js";
7
7
  export type { MemoryAuthOptions } from "./auth/memory-auth.adapter.js";
8
- export { CredentialInput, CredentialInputs } from "./auth/credential-input.entity.js";
9
- export { credentialsOf, toCredential, toCredentialInput } from "./auth/credential-input.entity.functions.js";
8
+ export { AccountId, CredentialInput, CredentialInputs, InstallationId } from "./auth/credential-input.entity.js";
10
9
  export { RefreshableProvider } from "./auth/credential-refresh.entity.js";
11
10
  export type { OAuthCredential } from "./auth/credential-refresh.entity.js";
12
11
  export { isUsableAt, REFRESH_SKEW_MS, refreshCredential } from "./auth/credential-refresh.entity.functions.js";
13
12
  export { HostCredentials } from "./ports/host-credentials.port.js";
13
+ export { AuthError, AuthStore, Credential } from "./ports/auth-store.port.js";
14
+ export { EngineSettings, SETTINGS_KEYS, SettingsError, SettingsStore } from "./ports/settings-store.port.js";
15
+ export type { ModelRole, SettingsKey } from "./ports/settings-store.port.js";
14
16
  export { LocalSettingsStoreLive } from "./settings/localSettings.js";
15
17
  export { MemorySettingsStoreLive } from "./settings/memory-settings.adapter.js";
16
18
  export { RoleField, SettingValue } from "./settings/settings-value.entity.js";
17
19
  export { applyRole, applySetting, coerceSettingValue, ROLE_FIELDS, roleSelection, settingsFromRecord } from "./settings/settings-value.entity.functions.js";
18
20
  export { roleModelView } from "./settings/roleView.js";
19
- export { fromChatCompletion, makeCompatLanguageModel, promptWithImageLabels, toChatMessages, toChatTools, } from "./llm/compat.js";
20
- export type { ChatMessageOptions, CompatConfig } from "./llm/compat.js";
21
- export type { UsageReading } from "./llm/sse.js";
22
- export { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL, toOpenAiCodexRequestBody } from "./llm/openAiCodex.js";
23
- export { makeOpenAiCodexWebSocketHttpClient, normalizeOpenAiCodexWebSocketEvent, openAiCodexRefusal, openAiCodexUuidV7, OpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
24
- export { buildProvider, chatCompletionsUrl, endpointOwnKey, OPENCODE_CHAT_URL, OPENCODE_RESPONSES_API_URL, prependClaudeCode, usesOpenCodeResponses, withAnthropicCacheBreakpoints, } from "./llm/providers.js";
25
- export { classifyLlmError, LLM_REQUEST_TIMEOUT_MS, MAX_HONORED_RETRY_AFTER_MS, rejectEmptyResponse, retryableLlm, } from "./llm/retry.js";
26
- export type { ErrorClass } from "./llm/retry.js";
27
- export { generateWith, LanguageModelLive, LanguageModelSelectionLive } from "./llm/router.js";
28
- export { configuredModelCatalog, ConfiguredModelCatalogLive, imageInputFor, reasoningEffortsFor, } from "./llm/modelCatalog.js";
21
+ export { makeOpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
22
+ export { LLM_CALL_BUDGET_MS, LLM_REQUEST_TIMEOUT_MS } from "./llm/retry.js";
23
+ export { ModelRetryPolicy, modelRetryDefaults } from "./llm/model-retry-policy.entity.js";
24
+ export { CurrentModelRetryPolicy } from "./ports/model-retry-policy.port.js";
25
+ export { LanguageModelLive, LanguageModelSelectionLive } from "./llm/router.js";
26
+ export { ConfiguredModelCatalogLive, imageInputFor, reasoningEffortsFor } from "./llm/modelCatalog.js";
29
27
  export type { ReasoningEffort } from "./llm/modelCatalog.js";
30
28
  export { UtilityLlmLive } from "./llm/utilityLlm.js";
31
29
  export { modelsPlugin, default } from "./plugin.adapter.js";
32
30
  export { CurrentModelEndpoints, ModelEndpoint, ModelEndpoints } from "./ports/model-endpoints.port.js";
33
31
  export { ModelsTransport } from "./ports/models-transport.port.js";
34
32
  export type { ModelsFetch, WebSocketProxy } from "./ports/models-transport.port.js";
35
- export { ModelsTransportLive, transportFetch } from "./transport/models-transport.adapter.js";
36
- export { BUN_WEBSOCKET_PROXY_VERSION, HttpProxyOptions, NODE_WEBSOCKET_PROXY_VERSION, NoProxyRule, PROXY_VARIABLES, ProxyConfig, ProxyEnvironment, ProxyInvalid, ProxyUnsupported, ProxyUrlOption, } from "./transport/http-proxy.entity.js";
37
- export { bypassesProxy, isLoopback, parseNoProxy, proxyFor, redactCredentials, resolveProxy } from "./transport/http-proxy.entity.functions.js";
33
+ export { ModelsTransportLive } from "./transport/models-transport.adapter.js";
34
+ export { HttpProxyOptions, ProxyInvalid, ProxyUnsupported, ProxyUrlOption } from "./transport/http-proxy.entity.js";
35
+ export { ModelThinking, ModelSettings, ModelSettingsMap, ModelSettingsError } from "./llm/model-settings.entity.js";
36
+ export { PromptCacheSettings } from "./llm/prompt-cache.entity.js";
37
+ export { CurrentModelSettings } from "./ports/model-settings.port.js";