@xandreed/plugin-models 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/anthropicOAuth.d.ts +16 -2
  3. package/dist/auth/anthropicOAuth.js +14 -11
  4. package/dist/auth/credential-input.entity.d.ts +34 -0
  5. package/dist/auth/credential-input.entity.functions.d.ts +8 -0
  6. package/dist/auth/credential-input.entity.functions.js +9 -0
  7. package/dist/auth/credential-input.entity.js +20 -0
  8. package/dist/auth/credential-refresh.entity.d.ts +9 -0
  9. package/dist/auth/credential-refresh.entity.functions.d.ts +15 -0
  10. package/dist/auth/credential-refresh.entity.functions.js +12 -0
  11. package/dist/auth/credential-refresh.entity.functions.test.d.ts +1 -0
  12. package/dist/auth/credential-refresh.entity.js +2 -0
  13. package/dist/auth/localAuth.d.ts +2 -1
  14. package/dist/auth/localAuth.js +41 -43
  15. package/dist/auth/memory-auth.adapter.d.ts +27 -0
  16. package/dist/auth/memory-auth.adapter.js +47 -0
  17. package/dist/auth/memory-auth.adapter.test.d.ts +1 -0
  18. package/dist/auth/openAiCodexOAuth.d.ts +3 -2
  19. package/dist/auth/openAiCodexOAuth.js +9 -8
  20. package/dist/index.d.ts +23 -6
  21. package/dist/index.js +28 -1
  22. package/dist/llm/compat.d.ts +39 -8
  23. package/dist/llm/compat.js +80 -29
  24. package/dist/llm/modelCatalog.d.ts +20 -1
  25. package/dist/llm/modelCatalog.js +19 -7
  26. package/dist/llm/openAiCodex.d.ts +4 -1
  27. package/dist/llm/openAiCodex.js +2 -2
  28. package/dist/llm/openAiCodexWebSocket.d.ts +23 -3
  29. package/dist/llm/openAiCodexWebSocket.js +125 -71
  30. package/dist/llm/openAiCodexWebSocket.test.d.ts +1 -0
  31. package/dist/llm/providers.d.ts +18 -4
  32. package/dist/llm/providers.js +49 -24
  33. package/dist/llm/router.d.ts +11 -1
  34. package/dist/llm/router.js +55 -31
  35. package/dist/llm/sse.d.ts +25 -3
  36. package/dist/llm/sse.js +8 -8
  37. package/dist/llm/utilityLlm.d.ts +1 -1
  38. package/dist/llm/utilityLlm.js +4 -4
  39. package/dist/plugin.adapter.d.ts +64 -4
  40. package/dist/plugin.adapter.js +48 -10
  41. package/dist/ports/host-credentials.port.d.ts +21 -0
  42. package/dist/ports/host-credentials.port.js +4 -0
  43. package/dist/ports/model-endpoints.port.d.ts +16 -3
  44. package/dist/ports/model-endpoints.port.js +7 -3
  45. package/dist/ports/models-transport.port.d.ts +46 -0
  46. package/dist/ports/models-transport.port.js +4 -0
  47. package/dist/settings/localSettings.js +6 -46
  48. package/dist/settings/memory-settings.adapter.d.ts +10 -0
  49. package/dist/settings/memory-settings.adapter.js +11 -0
  50. package/dist/settings/memory-settings.adapter.test.d.ts +1 -0
  51. package/dist/settings/settings-value.entity.d.ts +7 -0
  52. package/dist/settings/settings-value.entity.functions.d.ts +29 -0
  53. package/dist/settings/settings-value.entity.functions.js +43 -0
  54. package/dist/settings/settings-value.entity.functions.test.d.ts +1 -0
  55. package/dist/settings/settings-value.entity.js +2 -0
  56. package/dist/transport/http-proxy.entity.d.ts +90 -0
  57. package/dist/transport/http-proxy.entity.functions.d.ts +51 -0
  58. package/dist/transport/http-proxy.entity.functions.js +112 -0
  59. package/dist/transport/http-proxy.entity.functions.test.d.ts +1 -0
  60. package/dist/transport/http-proxy.entity.js +42 -0
  61. package/dist/transport/models-transport.adapter.d.ts +48 -0
  62. package/dist/transport/models-transport.adapter.js +102 -0
  63. package/dist/transport/models-transport.adapter.test.d.ts +1 -0
  64. package/dist/transport/recording-proxy.testing.d.ts +33 -0
  65. package/dist/transport/undici-fetch.adapter.d.ts +13 -0
  66. package/dist/transport/undici-fetch.adapter.js +1 -0
  67. package/package.json +4 -3
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @xandreed/plugin-models
2
2
 
3
- Efferent composable agent framework, 0.13.0. Node 24 or Bun.
3
+ Efferent composable agent framework, 0.14.0. Node 24.5 or Bun.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
@@ -1,5 +1,6 @@
1
1
  import { Effect, Option } from "effect";
2
2
  import { AuthError } from "@xandreed/core";
3
+ import type { ModelsFetch } from "../ports/models-transport.port.js";
3
4
  /**
4
5
  * Anthropic OAuth (Claude Pro/Max subscription) — the full protocol:
5
6
  * authorization-code + PKCE (begin/exchange) and refresh. Constants match
@@ -21,8 +22,21 @@ export interface RefreshedTokens {
21
22
  /** Absolute expiry, epoch ms (with a 5-minute early-refresh skew baked in). */
22
23
  readonly expires: number;
23
24
  }
25
+ /**
26
+ * How one token request is made, for both subscription providers. `fetch`
27
+ * replaces the global fetch (a test stub, or a `ModelsTransport`'s fetch, so
28
+ * the request goes through the models plugin's proxy); `now` is
29
+ * the time the new token's expiry is counted from, in epoch ms (default: the
30
+ * Effect `Clock`).
31
+ */
32
+ export interface TokenRequestOptions {
33
+ readonly fetch?: ModelsFetch;
34
+ readonly now?: number;
35
+ }
36
+ /** The time a token's expiry is counted from: the caller's, or the Effect Clock's. */
37
+ export declare const tokenTime: (options: TokenRequestOptions) => Effect.Effect<number>;
24
38
  /** Refresh an expired (or near-expiry) access token. */
25
- export declare const refreshAnthropicToken: (refreshToken: string) => Effect.Effect<RefreshedTokens, AuthError>;
39
+ export declare const refreshAnthropicToken: (refreshToken: string, options?: TokenRequestOptions) => Effect.Effect<RefreshedTokens, AuthError>;
26
40
  export interface Pkce {
27
41
  readonly verifier: string;
28
42
  readonly challenge: string;
@@ -40,7 +54,7 @@ export interface OAuthBegin {
40
54
  /** Start an authorization: PKCE material + the browser URL. */
41
55
  export declare const beginAnthropicOAuth: Effect.Effect<OAuthBegin>;
42
56
  /** Exchange the authorization code for tokens. */
43
- export declare const exchangeAnthropicCode: (code: string, verifier: string) => Effect.Effect<RefreshedTokens, AuthError>;
57
+ export declare const exchangeAnthropicCode: (code: string, verifier: string, options?: TokenRequestOptions) => Effect.Effect<RefreshedTokens, AuthError>;
44
58
  export interface AuthRedirect {
45
59
  readonly code: Option.Option<string>;
46
60
  readonly state: Option.Option<string>;
@@ -1,5 +1,6 @@
1
- import { Effect, Option } from "effect";
1
+ import { Clock, Effect, Option } from "effect";
2
2
  import { AuthError } from "@xandreed/core";
3
+ import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
3
4
  export const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
4
5
  const AUTHORIZE_URL = "https://claude.ai/oauth/authorize", TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
5
6
  export const ANTHROPIC_CALLBACK_PORT = 53692, ANTHROPIC_CALLBACK_PATH = "/callback", ANTHROPIC_REDIRECT_URI = `http://localhost:${ANTHROPIC_CALLBACK_PORT}${ANTHROPIC_CALLBACK_PATH}`;
@@ -8,11 +9,13 @@ export const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20", CLA
8
9
  const oauthError = (stage) => (detail) => new AuthError({
9
10
  provider: "anthropic",
10
11
  message: `OAuth ${stage} failed: ${detail}`
11
- }), postToken = (body, stage) => {
12
+ });
13
+ export const tokenTime = (options) => options.now === void 0 ? Clock.currentTimeMillis : Effect.succeed(options.now);
14
+ const postToken = (body, stage, options) => {
12
15
  const fail = oauthError(stage);
13
16
  return Effect.tryPromise({
14
17
  try: async () => {
15
- const res = await fetch(TOKEN_URL, {
18
+ const res = await (options.fetch ?? fetch)(TOKEN_URL, {
16
19
  method: "POST",
17
20
  headers: { "Content-Type": "application/json", Accept: "application/json" },
18
21
  body: JSON.stringify(body),
@@ -20,21 +23,21 @@ const oauthError = (stage) => (detail) => new AuthError({
20
23
  });
21
24
  return { ok: res.ok, status: res.status, text: await res.text() };
22
25
  },
23
- catch: (e) => fail(String(e))
26
+ catch: (e) => fail(redactCredentials(String(e)))
24
27
  }).pipe(Effect.filterOrFail((res) => res.ok, (res) => fail(`status ${res.status}: ${res.text.slice(0, 200)}`)), Effect.flatMap((res) => Effect.try({
25
28
  try: () => JSON.parse(res.text),
26
29
  catch: () => fail("the token response was not JSON")
27
- })), Effect.map((data) => ({
30
+ })), Effect.flatMap((data) => Effect.map(tokenTime(options), (now) => ({
28
31
  access: data.access_token,
29
32
  refresh: data.refresh_token,
30
- expires: Date.now() + data.expires_in * 1000 - 300000
31
- })));
33
+ expires: now + data.expires_in * 1000 - 300000
34
+ }))));
32
35
  };
33
- export const refreshAnthropicToken = (refreshToken) => postToken({
36
+ export const refreshAnthropicToken = (refreshToken, options = {}) => postToken({
34
37
  grant_type: "refresh_token",
35
38
  client_id: ANTHROPIC_CLIENT_ID,
36
39
  refresh_token: refreshToken
37
- }, "token refresh");
40
+ }, "token refresh", options);
38
41
  const base64url = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
39
42
  export const generatePkce = Effect.promise(async () => {
40
43
  const verifierBytes = new Uint8Array(32);
@@ -55,14 +58,14 @@ export const generatePkce = Effect.promise(async () => {
55
58
  }).toString()}`,
56
59
  callbackPort: ANTHROPIC_CALLBACK_PORT,
57
60
  callbackPath: ANTHROPIC_CALLBACK_PATH
58
- })), exchangeAnthropicCode = (code, verifier) => postToken({
61
+ })), exchangeAnthropicCode = (code, verifier, options = {}) => postToken({
59
62
  grant_type: "authorization_code",
60
63
  client_id: ANTHROPIC_CLIENT_ID,
61
64
  code,
62
65
  redirect_uri: ANTHROPIC_REDIRECT_URI,
63
66
  code_verifier: verifier,
64
67
  state: verifier
65
- }, "code exchange");
68
+ }, "code exchange", options);
66
69
  const fromNullableTrimmed = (value) => Option.filter(Option.fromNullishOr(value), (v) => v.trim().length > 0);
67
70
  export const parseAuthorizationInput = (input) => {
68
71
  const value = input.trim();
@@ -0,0 +1,34 @@
1
+ import { Schema } from "effect";
2
+ export declare const CredentialInput: Schema.Union<readonly [Schema.Struct<{
3
+ readonly type: Schema.Literal<"api_key">;
4
+ readonly key: Schema.Redacted<Schema.String>;
5
+ }>, Schema.Struct<{
6
+ readonly type: Schema.Literal<"oauth">;
7
+ readonly access: Schema.Redacted<Schema.String>;
8
+ readonly refresh: Schema.Redacted<Schema.String>;
9
+ /** Absolute expiry, epoch ms. */
10
+ readonly expires: Schema.Number;
11
+ readonly accountId: Schema.optionalKey<Schema.String>;
12
+ readonly installationId: Schema.optionalKey<Schema.String>;
13
+ }>, Schema.Struct<{
14
+ readonly type: Schema.Literal<"local">;
15
+ readonly baseUrl: Schema.optionalKey<Schema.String>;
16
+ }>]>;
17
+ export type CredentialInput = typeof CredentialInput.Type;
18
+ /** Credentials by provider id. */
19
+ export declare const CredentialInputs: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.Struct<{
20
+ readonly type: Schema.Literal<"api_key">;
21
+ readonly key: Schema.Redacted<Schema.String>;
22
+ }>, Schema.Struct<{
23
+ readonly type: Schema.Literal<"oauth">;
24
+ readonly access: Schema.Redacted<Schema.String>;
25
+ readonly refresh: Schema.Redacted<Schema.String>;
26
+ /** Absolute expiry, epoch ms. */
27
+ readonly expires: Schema.Number;
28
+ readonly accountId: Schema.optionalKey<Schema.String>;
29
+ readonly installationId: Schema.optionalKey<Schema.String>;
30
+ }>, Schema.Struct<{
31
+ readonly type: Schema.Literal<"local">;
32
+ readonly baseUrl: Schema.optionalKey<Schema.String>;
33
+ }>]>>;
34
+ export type CredentialInputs = typeof CredentialInputs.Type;
@@ -0,0 +1,8 @@
1
+ import type { Credential } from "@xandreed/core";
2
+ import type { CredentialInput, CredentialInputs } from "./credential-input.entity.js";
3
+ /** The stored form of a credential a host handed over: its secrets unwrapped. */
4
+ export declare const toCredential: (input: CredentialInput) => Credential;
5
+ /** A stored credential as a host receives it: its secrets `Redacted`. */
6
+ export declare const toCredentialInput: (credential: Credential) => CredentialInput;
7
+ /** Handed-over credentials, by provider id, in their stored form. */
8
+ export declare const credentialsOf: (inputs: CredentialInputs) => ReadonlyMap<string, Credential>;
@@ -0,0 +1,9 @@
1
+ import { Match, Redacted } from "effect";
2
+ export const toCredential = (input) => Match.value(input).pipe(Match.when({ type: "api_key" }, ({ key }) => ({ type: "api_key", key: Redacted.value(key) })), Match.when({ type: "oauth" }, ({ access, refresh, ...rest }) => ({ ...rest, access: Redacted.value(access), refresh: Redacted.value(refresh) })), Match.when({ type: "local" }, (local) => local), Match.exhaustive), toCredentialInput = (credential) => Match.value(credential).pipe(Match.when({ type: "api_key" }, ({ key }) => ({ type: "api_key", key: Redacted.make(key) })), Match.when({ type: "oauth" }, ({ type, access, refresh, expires, accountId, installationId }) => ({
3
+ type,
4
+ expires,
5
+ access: Redacted.make(access),
6
+ refresh: Redacted.make(refresh),
7
+ ...accountId === void 0 ? {} : { accountId },
8
+ ...installationId === void 0 ? {} : { installationId }
9
+ })), Match.when({ type: "local" }, ({ type, baseUrl }) => ({ type, ...baseUrl === void 0 ? {} : { baseUrl } })), Match.exhaustive), credentialsOf = (inputs) => new Map(Object.entries(inputs).map(([provider, input]) => [provider, toCredential(input)]));
@@ -0,0 +1,20 @@
1
+ import { Schema } from "effect";
2
+ const secret = Schema.Redacted(Schema.String).annotate({
3
+ message: "a secret must be a Redacted value (Redacted.make(...)); JSON configuration names an environment variable in credentialEnv instead"
4
+ }), unexpectedKey = { messageUnexpectedKey: "not a field of a credential" };
5
+ export const CredentialInput = Schema.Union([
6
+ Schema.Struct({ type: Schema.Literal("api_key"), key: secret }).annotate(unexpectedKey),
7
+ Schema.Struct({
8
+ type: Schema.Literal("oauth"),
9
+ access: secret,
10
+ refresh: secret,
11
+ expires: Schema.Number.annotate({ message: "expires is the access token's expiry, in epoch ms" }).check(Schema.isFinite()),
12
+ accountId: Schema.optionalKey(Schema.String),
13
+ installationId: Schema.optionalKey(Schema.String)
14
+ }).annotate(unexpectedKey),
15
+ Schema.Struct({ type: Schema.Literal("local"), baseUrl: Schema.optionalKey(Schema.String) }).annotate(unexpectedKey)
16
+ ]).annotate({
17
+ message: 'a credential is { type: "api_key", key }, { type: "oauth", access, refresh, expires, accountId?, installationId? } or { type: "local", baseUrl? }, with Redacted secrets'
18
+ }), CredentialInputs = Schema.Record(Schema.String, CredentialInput).annotate({
19
+ message: "credentials maps provider ids to credentials"
20
+ });
@@ -0,0 +1,9 @@
1
+ import { Schema } from "effect";
2
+ import type { Credential } from "@xandreed/core";
3
+ /** The providers whose OAuth tokens a store can refresh: each has its token endpoint in an auth module. */
4
+ export declare const RefreshableProvider: Schema.Literals<readonly ["anthropic", "openai-codex"]>;
5
+ export type RefreshableProvider = typeof RefreshableProvider.Type;
6
+ /** An OAuth subscription credential: an access token, its refresh token and its expiry (epoch ms). */
7
+ export type OAuthCredential = Extract<Credential, {
8
+ readonly type: "oauth";
9
+ }>;
@@ -0,0 +1,15 @@
1
+ import { Effect } from "effect";
2
+ import { AuthError } from "@xandreed/core";
3
+ import type { TokenRequestOptions } from "./anthropicOAuth.js";
4
+ import type { OAuthCredential } from "./credential-refresh.entity.js";
5
+ /** Refresh this many ms before the recorded expiry. */
6
+ export declare const REFRESH_SKEW_MS = 60000;
7
+ /** Whether an OAuth credential can still be used at `now`: more than the skew before its expiry. */
8
+ export declare const isUsableAt: (credential: OAuthCredential, now: number) => boolean;
9
+ /**
10
+ * Refresh one provider's OAuth credential through that provider's token
11
+ * endpoint: the same credential with the new access token, refresh token
12
+ * and expiry (and account id, where the provider sends one). A provider
13
+ * with no refresh flow fails with an `AuthError` naming it.
14
+ */
15
+ export declare const refreshCredential: (provider: string, credential: OAuthCredential, options?: TokenRequestOptions) => Effect.Effect<OAuthCredential, AuthError>;
@@ -0,0 +1,12 @@
1
+ import { Effect, Match, Option, Schema } from "effect";
2
+ import { AuthError } from "@xandreed/core";
3
+ import { refreshAnthropicToken } from "./anthropicOAuth.js";
4
+ import { RefreshableProvider } from "./credential-refresh.entity.js";
5
+ import { refreshOpenAiCodexToken } from "./openAiCodexOAuth.js";
6
+ export const REFRESH_SKEW_MS = 60000, isUsableAt = (credential, now) => credential.expires > now + REFRESH_SKEW_MS, refreshCredential = (provider, credential, options = {}) => Option.match(Schema.decodeUnknownOption(RefreshableProvider)(provider), {
7
+ onNone: () => Effect.fail(new AuthError({
8
+ provider,
9
+ message: `the ${provider} OAuth token expired and only anthropic and openai-codex tokens can be refreshed \u2014 sign in again or use an API key`
10
+ })),
11
+ onSome: (refreshable) => Match.value(refreshable).pipe(Match.when("anthropic", () => refreshAnthropicToken(credential.refresh, options)), Match.when("openai-codex", () => refreshOpenAiCodexToken(credential.refresh, options)), Match.exhaustive).pipe(Effect.map((fresh) => ({ ...credential, ...fresh })))
12
+ });
@@ -0,0 +1,2 @@
1
+ import { Schema } from "effect";
2
+ export const RefreshableProvider = Schema.Literals(["anthropic", "openai-codex"]);
@@ -3,6 +3,7 @@ import { AuthStore } from "@xandreed/core";
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
- * so a credential added mid-session applies on the next request.
6
+ * so a credential added mid-session applies on the next request. A token
7
+ * refresh goes through the `ModelsTransport` the layer is built over, if any.
7
8
  */
8
9
  export declare const LocalAuthStoreLive: (cwd: string, home: string, directory?: string, fallbackDirectory?: string) => Layer.Layer<AuthStore, never, never>;
@@ -1,9 +1,9 @@
1
1
  import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { Effect, Layer, Option, Redacted } from "effect";
3
+ import { Clock, Effect, Layer, Option, Redacted } from "effect";
4
4
  import { asJsonRecord, AuthError, AuthStore, parseJsonWarn } from "@xandreed/core";
5
- import { refreshAnthropicToken } from "./anthropicOAuth.js";
6
- import { refreshOpenAiCodexToken } from "./openAiCodexOAuth.js";
5
+ import { isUsableAt, refreshCredential } from "./credential-refresh.entity.functions.js";
6
+ import { ModelsTransport } from "../ports/models-transport.port.js";
7
7
  const decodeCredential = (value) => {
8
8
  if (typeof value === "string")
9
9
  return Option.some({ type: "api_key", key: value });
@@ -42,49 +42,47 @@ const decodeCredential = (value) => {
42
42
  await rename(tmp, path);
43
43
  },
44
44
  catch: (e) => new AuthError({ provider: "*", message: `auth.json write failed: ${String(e)}` })
45
- }), REFRESH_SKEW_MS = 60000;
45
+ });
46
46
  export const authPaths = (cwd, home, directory = ".efferent") => [
47
47
  join(home, directory, "auth.json"),
48
48
  join(cwd, directory, "auth.json")
49
49
  ], LocalAuthStoreLive = (cwd, home, directory = ".efferent", fallbackDirectory) => {
50
50
  const paths = [...fallbackDirectory === void 0 ? [] : authPaths(cwd, home, fallbackDirectory), ...authPaths(cwd, home, directory)];
51
- return Layer.succeed(AuthStore, {
52
- all: Effect.gen(function* () {
53
- const files = yield* Effect.forEach(paths, readAuthFile);
54
- return new Map(files.flatMap((f) => [...f.entries]));
55
- }),
56
- get: (provider) => Effect.gen(function* () {
57
- const files = yield* Effect.forEach(paths, readAuthFile), merged = new Map(files.flatMap((f) => [...f.entries]));
58
- return Option.fromNullishOr(merged.get(provider));
59
- }),
60
- resolveKey: (provider) => Effect.gen(function* () {
61
- const holder = [...yield* Effect.forEach(paths, readAuthFile)].reverse().find((f) => f.entries.has(provider)), cred = holder?.entries.get(provider);
62
- if (cred === void 0)
63
- return Option.none();
64
- if (cred.type === "api_key")
65
- return Option.some(Redacted.make(cred.key));
66
- if (cred.type === "local")
67
- return Option.none();
68
- if (cred.expires > Date.now() + REFRESH_SKEW_MS)
69
- return Option.some(Redacted.make(cred.access));
70
- if (provider !== "anthropic" && provider !== "openai-codex")
71
- return yield* Effect.fail(new AuthError({
72
- provider,
73
- message: `the ${provider} OAuth token expired and its refresh flow isn't wired on the new line \u2014 re-login or use an API key`
74
- }));
75
- const fresh = yield* provider === "anthropic" ? refreshAnthropicToken(cred.refresh) : refreshOpenAiCodexToken(cred.refresh), updated = { ...cred, ...fresh }, target = holder !== void 0 && authPaths(cwd, home, directory).includes(holder.path) ? holder : yield* readAuthFile(authPaths(cwd, home, directory)[0] ?? "");
76
- yield* writeAuthFile(target.path, new Map([...target.entries, [provider, updated]]));
77
- return Option.some(Redacted.make(fresh.access));
78
- }),
79
- set: (provider, credential) => Effect.gen(function* () {
80
- const globalPath = authPaths(cwd, home, directory)[0] ?? "", file = yield* readAuthFile(globalPath);
81
- yield* writeAuthFile(globalPath, new Map([...file.entries, [provider, credential]]));
82
- }),
83
- remove: (provider) => Effect.forEach(paths, (path) => Effect.gen(function* () {
84
- const file = yield* readAuthFile(path);
85
- if (!file.entries.has(provider))
86
- return;
87
- yield* writeAuthFile(path, new Map([...file.entries].filter(([p]) => p !== provider)));
88
- })).pipe(Effect.asVoid)
89
- });
51
+ return Layer.effect(AuthStore, Effect.map(Effect.serviceOption(ModelsTransport), (transport) => {
52
+ const tokens = Option.match(transport, { onNone: () => ({}), onSome: ({ fetch }) => ({ fetch }) });
53
+ 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)
86
+ };
87
+ }));
90
88
  };
@@ -0,0 +1,27 @@
1
+ import { Layer } from "effect";
2
+ import { AuthStore } from "@xandreed/core";
3
+ import type { AuthError, Credential } from "@xandreed/core";
4
+ import type { ModelsFetch } from "../ports/models-transport.port.js";
5
+ /** How the in-memory store reaches the OAuth token endpoints. */
6
+ export interface MemoryAuthOptions {
7
+ /** The fetch token refreshes go through; default: the `ModelsTransport`'s when the layer is built over one, else the global fetch. */
8
+ readonly fetch?: ModelsFetch;
9
+ }
10
+ /**
11
+ * `MemoryAuthStoreLive(seed?)` — an AuthStore that never touches disk.
12
+ * Credentials live in memory for the layer's lifetime: the host's
13
+ * `HostCredentials.load` (when the host provides that service), overlaid by
14
+ * `seed`. `resolveKey` refreshes a near-expiry anthropic or openai-codex
15
+ * OAuth token in memory, one refresh per provider at a time (through the
16
+ * `ModelsTransport` it was built over, so through the plugin's proxy), and
17
+ * hands the new token to `HostCredentials.save`.
18
+ *
19
+ * A login or logout (`set`, `remove`) does not wait for a refresh's token
20
+ * request; it waits only while a refreshed token is committed, which
21
+ * includes the host's `save` of it, so a slow host store delays it that
22
+ * long. A refresh writes its token only if the provider still holds the
23
+ * credential it refreshed (compare-and-set under the provider's lock): a
24
+ * logout during a stalled token request stays a logout, and the host never
25
+ * receives the refreshed token.
26
+ */
27
+ export declare const MemoryAuthStoreLive: (seed?: ReadonlyMap<string, Credential>, options?: MemoryAuthOptions) => Layer.Layer<AuthStore, AuthError>;
@@ -0,0 +1,47 @@
1
+ import { Clock, Effect, Layer, Match, Option, Redacted, Ref, Semaphore, SynchronizedRef } from "effect";
2
+ import { AuthStore } from "@xandreed/core";
3
+ import { HostCredentials } from "../ports/host-credentials.port.js";
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";
7
+ export const MemoryAuthStoreLive = (seed = new Map, options = {}) => Layer.effect(AuthStore, Effect.gen(function* () {
8
+ const host = yield* Effect.serviceOption(HostCredentials), loaded = yield* Option.match(host, {
9
+ onNone: () => Effect.succeed(new Map),
10
+ onSome: (credentials) => credentials.load.pipe(Effect.map((all) => new Map([...all].map(([provider, input]) => [provider, toCredential(input)]))))
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
+ onNone: () => ({}),
13
+ onSome: (fetch) => ({ fetch })
14
+ }), locksOf = (provider) => SynchronizedRef.modifyEffect(locks, (all) => Option.match(Option.fromNullishOr(all.get(provider)), {
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]])]))
17
+ })), current = (provider) => Ref.get(state).pipe(Effect.map((all) => Option.fromNullishOr(all.get(provider)))), save = (provider, credential) => Option.match(host, {
18
+ onNone: () => Effect.void,
19
+ onSome: (credentials) => credentials.save(provider, Option.map(credential, toCredentialInput))
20
+ }), write = (provider, credential) => Ref.update(state, (all) => Option.match(credential, {
21
+ onNone: () => new Map([...all].filter(([key]) => key !== provider)),
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
+ })));
40
+ return AuthStore.of({
41
+ all: Ref.get(state),
42
+ 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))))))),
45
+ 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
+ });
47
+ }));
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { Effect, Option } from "effect";
2
2
  import { AuthError } from "@xandreed/core";
3
+ import type { TokenRequestOptions } from "./anthropicOAuth.js";
3
4
  /** ChatGPT Plus/Pro OAuth protocol used by the OpenAI Codex subscription. */
4
5
  export declare const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
5
6
  export declare const OPENAI_CODEX_CALLBACK_PORT = 1455;
@@ -21,5 +22,5 @@ export interface OpenAiCodexOAuthBegin {
21
22
  /** Extract the subscription account lane without exposing the token. */
22
23
  export declare const openAiCodexAccountId: (accessToken: string) => Option.Option<string>;
23
24
  export declare const beginOpenAiCodexOAuth: Effect.Effect<OpenAiCodexOAuthBegin>;
24
- export declare const exchangeOpenAiCodexCode: (code: string, verifier: string) => Effect.Effect<OpenAiCodexTokens, AuthError>;
25
- export declare const refreshOpenAiCodexToken: (refreshToken: string) => Effect.Effect<OpenAiCodexTokens, AuthError>;
25
+ 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>;
@@ -1,6 +1,7 @@
1
1
  import { Effect, Result, Option } from "effect";
2
2
  import { AuthError } from "@xandreed/core";
3
- import { generatePkce } from "./anthropicOAuth.js";
3
+ import { generatePkce, tokenTime } from "./anthropicOAuth.js";
4
+ import { redactCredentials } from "../transport/http-proxy.entity.functions.js";
4
5
  export const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
5
6
  const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize", TOKEN_URL = "https://auth.openai.com/oauth/token";
6
7
  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}`;
@@ -18,9 +19,9 @@ export const openAiCodexAccountId = (accessToken) => {
18
19
  return Option.filter(Option.fromNullishOr(typeof accountId === "string" ? accountId : void 0), (value) => value.length > 0);
19
20
  }));
20
21
  };
21
- const postToken = (body, stage) => Effect.tryPromise({
22
+ const postToken = (body, stage, options) => Effect.tryPromise({
22
23
  try: async () => {
23
- const response = await fetch(TOKEN_URL, {
24
+ const response = await (options.fetch ?? fetch)(TOKEN_URL, {
24
25
  method: "POST",
25
26
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
26
27
  body: new URLSearchParams(body),
@@ -28,16 +29,16 @@ const postToken = (body, stage) => Effect.tryPromise({
28
29
  });
29
30
  return { ok: response.ok, status: response.status, text: await response.text() };
30
31
  },
31
- catch: (error) => oauthError(stage, String(error))
32
+ catch: (error) => oauthError(stage, redactCredentials(String(error)))
32
33
  }).pipe(Effect.filterOrFail((response) => response.ok, (response) => oauthError(stage, `status ${response.status}: ${response.text.slice(0, 200)}`)), Effect.flatMap((response) => Effect.try({
33
34
  try: () => JSON.parse(response.text),
34
35
  catch: () => oauthError(stage, "the token response was not JSON")
35
- })), 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.map((token) => ({
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) => ({
36
37
  access: token.access_token,
37
38
  refresh: token.refresh_token,
38
- expires: Date.now() + token.expires_in * 1000,
39
+ expires: now + token.expires_in * 1000,
39
40
  ...Option.match(openAiCodexAccountId(token.access_token), { onNone: () => ({}), onSome: (accountId) => ({ accountId }) })
40
- })));
41
+ }))));
41
42
  export const beginOpenAiCodexOAuth = Effect.gen(function* () {
42
43
  const pkce = yield* generatePkce, stateBytes = new Uint8Array(16);
43
44
  crypto.getRandomValues(stateBytes);
@@ -55,4 +56,4 @@ export const beginOpenAiCodexOAuth = Effect.gen(function* () {
55
56
  originator: "pi"
56
57
  }).forEach(([key, value]) => url.searchParams.set(key, value));
57
58
  return { verifier: pkce.verifier, state, authorizeUrl: url.toString(), callbackPort: OPENAI_CODEX_CALLBACK_PORT, callbackPath: OPENAI_CODEX_CALLBACK_PATH };
58
- }), exchangeOpenAiCodexCode = (code, verifier) => postToken({ grant_type: "authorization_code", client_id: OPENAI_CODEX_CLIENT_ID, code, code_verifier: verifier, redirect_uri: OPENAI_CODEX_REDIRECT_URI }, "code exchange"), refreshOpenAiCodexToken = (refreshToken) => postToken({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: OPENAI_CODEX_CLIENT_ID }, "token refresh");
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);
package/dist/index.d.ts CHANGED
@@ -1,20 +1,37 @@
1
1
  export { ANTHROPIC_OAUTH_BETA, beginAnthropicOAuth, CLAUDE_CODE_SYSTEM, exchangeAnthropicCode, generatePkce, parseAuthorizationInput, refreshAnthropicToken, } from "./auth/anthropicOAuth.js";
2
- export type { AuthRedirect, OAuthBegin, Pkce, RefreshedTokens } from "./auth/anthropicOAuth.js";
2
+ export type { AuthRedirect, OAuthBegin, Pkce, RefreshedTokens, TokenRequestOptions } from "./auth/anthropicOAuth.js";
3
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";
4
4
  export type { OpenAiCodexOAuthBegin, OpenAiCodexTokens } from "./auth/openAiCodexOAuth.js";
5
5
  export { authPaths, LocalAuthStoreLive } from "./auth/localAuth.js";
6
+ export { MemoryAuthStoreLive } from "./auth/memory-auth.adapter.js";
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";
10
+ export { RefreshableProvider } from "./auth/credential-refresh.entity.js";
11
+ export type { OAuthCredential } from "./auth/credential-refresh.entity.js";
12
+ export { isUsableAt, REFRESH_SKEW_MS, refreshCredential } from "./auth/credential-refresh.entity.functions.js";
13
+ export { HostCredentials } from "./ports/host-credentials.port.js";
6
14
  export { LocalSettingsStoreLive } from "./settings/localSettings.js";
15
+ export { MemorySettingsStoreLive } from "./settings/memory-settings.adapter.js";
16
+ export { RoleField, SettingValue } from "./settings/settings-value.entity.js";
17
+ export { applyRole, applySetting, coerceSettingValue, ROLE_FIELDS, roleSelection, settingsFromRecord } from "./settings/settings-value.entity.functions.js";
7
18
  export { roleModelView } from "./settings/roleView.js";
8
- export { fromChatCompletion, makeCompatLanguageModel, toChatMessages, toChatTools, } from "./llm/compat.js";
9
- export type { CompatConfig } from "./llm/compat.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";
10
22
  export { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL, toOpenAiCodexRequestBody } from "./llm/openAiCodex.js";
11
- export { normalizeOpenAiCodexWebSocketEvent, openAiCodexUuidV7, OpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
12
- export { buildProvider, chatCompletionsUrl, OPENCODE_CHAT_URL, OPENCODE_RESPONSES_API_URL, prependClaudeCode, usesOpenCodeResponses, withAnthropicCacheBreakpoints, } from "./llm/providers.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";
13
25
  export { classifyLlmError, LLM_REQUEST_TIMEOUT_MS, MAX_HONORED_RETRY_AFTER_MS, rejectEmptyResponse, retryableLlm, } from "./llm/retry.js";
14
26
  export type { ErrorClass } from "./llm/retry.js";
15
27
  export { generateWith, LanguageModelLive, LanguageModelSelectionLive } from "./llm/router.js";
16
- export { configuredModelCatalog, ConfiguredModelCatalogLive, reasoningEffortsFor, } from "./llm/modelCatalog.js";
28
+ export { configuredModelCatalog, ConfiguredModelCatalogLive, imageInputFor, reasoningEffortsFor, } from "./llm/modelCatalog.js";
17
29
  export type { ReasoningEffort } from "./llm/modelCatalog.js";
18
30
  export { UtilityLlmLive } from "./llm/utilityLlm.js";
19
31
  export { modelsPlugin, default } from "./plugin.adapter.js";
20
32
  export { CurrentModelEndpoints, ModelEndpoint, ModelEndpoints } from "./ports/model-endpoints.port.js";
33
+ export { ModelsTransport } from "./ports/models-transport.port.js";
34
+ 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";
package/dist/index.js CHANGED
@@ -18,19 +18,30 @@ export {
18
18
  refreshOpenAiCodexToken
19
19
  } from "./auth/openAiCodexOAuth.js";
20
20
  export { authPaths, LocalAuthStoreLive } from "./auth/localAuth.js";
21
+ export { MemoryAuthStoreLive } from "./auth/memory-auth.adapter.js";
22
+ export { CredentialInput, CredentialInputs } from "./auth/credential-input.entity.js";
23
+ export { credentialsOf, toCredential, toCredentialInput } from "./auth/credential-input.entity.functions.js";
24
+ export { RefreshableProvider } from "./auth/credential-refresh.entity.js";
25
+ export { isUsableAt, REFRESH_SKEW_MS, refreshCredential } from "./auth/credential-refresh.entity.functions.js";
26
+ export { HostCredentials } from "./ports/host-credentials.port.js";
21
27
  export { LocalSettingsStoreLive } from "./settings/localSettings.js";
28
+ export { MemorySettingsStoreLive } from "./settings/memory-settings.adapter.js";
29
+ export { RoleField, SettingValue } from "./settings/settings-value.entity.js";
30
+ export { applyRole, applySetting, coerceSettingValue, ROLE_FIELDS, roleSelection, settingsFromRecord } from "./settings/settings-value.entity.functions.js";
22
31
  export { roleModelView } from "./settings/roleView.js";
23
32
  export {
24
33
  fromChatCompletion,
25
34
  makeCompatLanguageModel,
35
+ promptWithImageLabels,
26
36
  toChatMessages,
27
37
  toChatTools
28
38
  } from "./llm/compat.js";
29
39
  export { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL, toOpenAiCodexRequestBody } from "./llm/openAiCodex.js";
30
- export { normalizeOpenAiCodexWebSocketEvent, openAiCodexUuidV7, OpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
40
+ export { makeOpenAiCodexWebSocketHttpClient, normalizeOpenAiCodexWebSocketEvent, openAiCodexRefusal, openAiCodexUuidV7, OpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
31
41
  export {
32
42
  buildProvider,
33
43
  chatCompletionsUrl,
44
+ endpointOwnKey,
34
45
  OPENCODE_CHAT_URL,
35
46
  OPENCODE_RESPONSES_API_URL,
36
47
  prependClaudeCode,
@@ -48,8 +59,24 @@ export { generateWith, LanguageModelLive, LanguageModelSelectionLive } from "./l
48
59
  export {
49
60
  configuredModelCatalog,
50
61
  ConfiguredModelCatalogLive,
62
+ imageInputFor,
51
63
  reasoningEffortsFor
52
64
  } from "./llm/modelCatalog.js";
53
65
  export { UtilityLlmLive } from "./llm/utilityLlm.js";
54
66
  export { modelsPlugin, default } from "./plugin.adapter.js";
55
67
  export { CurrentModelEndpoints, ModelEndpoint, ModelEndpoints } from "./ports/model-endpoints.port.js";
68
+ export { ModelsTransport } from "./ports/models-transport.port.js";
69
+ export { ModelsTransportLive, transportFetch } from "./transport/models-transport.adapter.js";
70
+ export {
71
+ BUN_WEBSOCKET_PROXY_VERSION,
72
+ HttpProxyOptions,
73
+ NODE_WEBSOCKET_PROXY_VERSION,
74
+ NoProxyRule,
75
+ PROXY_VARIABLES,
76
+ ProxyConfig,
77
+ ProxyEnvironment,
78
+ ProxyInvalid,
79
+ ProxyUnsupported,
80
+ ProxyUrlOption
81
+ } from "./transport/http-proxy.entity.js";
82
+ export { bypassesProxy, isLoopback, parseNoProxy, proxyFor, redactCredentials, resolveProxy } from "./transport/http-proxy.entity.functions.js";