@xandreed/plugin-models 0.3.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/auth/anthropicOAuth.d.ts +50 -0
  4. package/dist/auth/anthropicOAuth.js +90 -0
  5. package/dist/auth/anthropicOAuth.test.d.ts +1 -0
  6. package/dist/auth/localAuth.d.ts +8 -0
  7. package/dist/auth/localAuth.js +90 -0
  8. package/dist/auth/localAuth.test.d.ts +1 -0
  9. package/dist/auth/openAiCodexOAuth.d.ts +25 -0
  10. package/dist/auth/openAiCodexOAuth.js +58 -0
  11. package/dist/index.d.ts +19 -0
  12. package/dist/index.js +53 -0
  13. package/dist/llm/compat.d.ts +77 -0
  14. package/dist/llm/compat.js +215 -0
  15. package/dist/llm/compat.test.d.ts +1 -0
  16. package/dist/llm/modelCatalog.d.ts +10 -0
  17. package/dist/llm/modelCatalog.js +58 -0
  18. package/dist/llm/modelCatalog.test.d.ts +1 -0
  19. package/dist/llm/openAiCodex.d.ts +18 -0
  20. package/dist/llm/openAiCodex.js +99 -0
  21. package/dist/llm/openAiCodexWebSocket.d.ts +14 -0
  22. package/dist/llm/openAiCodexWebSocket.js +126 -0
  23. package/dist/llm/providers.d.ts +37 -0
  24. package/dist/llm/providers.js +114 -0
  25. package/dist/llm/providers.test.d.ts +1 -0
  26. package/dist/llm/retry.d.ts +54 -0
  27. package/dist/llm/retry.js +61 -0
  28. package/dist/llm/retry.test.d.ts +1 -0
  29. package/dist/llm/router.d.ts +55 -0
  30. package/dist/llm/router.js +125 -0
  31. package/dist/llm/router.test.d.ts +1 -0
  32. package/dist/llm/sse.d.ts +45 -0
  33. package/dist/llm/sse.js +116 -0
  34. package/dist/llm/sse.test.d.ts +1 -0
  35. package/dist/llm/utilityLlm.d.ts +8 -0
  36. package/dist/llm/utilityLlm.js +33 -0
  37. package/dist/plugin.adapter.d.ts +2 -0
  38. package/dist/plugin.adapter.js +27 -0
  39. package/dist/plugin.adapter.test.d.ts +1 -0
  40. package/dist/settings/localSettings.d.ts +3 -0
  41. package/dist/settings/localSettings.js +72 -0
  42. package/dist/settings/localSettings.test.d.ts +1 -0
  43. package/dist/settings/roleView.d.ts +11 -0
  44. package/dist/settings/roleView.js +11 -0
  45. package/dist/settings/roleView.test.d.ts +1 -0
  46. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xand Reed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @xandreed/plugin-models
2
+
3
+ Efferent composable agent framework, 0.3.0. Bun on Linux.
4
+
5
+ Documentation: https://github.com/xandreeddev/efferent
@@ -0,0 +1,50 @@
1
+ import { Effect, Option } from "effect";
2
+ import { AuthError } from "@xandreed/core";
3
+ /**
4
+ * Anthropic OAuth (Claude Pro/Max subscription) — the full protocol:
5
+ * authorization-code + PKCE (begin/exchange) and refresh. Constants match
6
+ * the public Claude Code OAuth client (the client id is not a secret).
7
+ * These are pure protocol helpers; the loopback callback server and the
8
+ * browser-open live in the TUI driver.
9
+ */
10
+ export declare const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
11
+ export declare const ANTHROPIC_CALLBACK_PORT = 53692;
12
+ export declare const ANTHROPIC_CALLBACK_PATH = "/callback";
13
+ export declare const ANTHROPIC_REDIRECT_URI = "http://localhost:53692/callback";
14
+ /** Beta flags the subscription path requires (Claude Code identity + oauth). */
15
+ export declare const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20";
16
+ /** Anthropic rejects OAuth tokens unless the first system block is exactly this. */
17
+ export declare const CLAUDE_CODE_SYSTEM = "You are Claude Code, Anthropic's official CLI for Claude.";
18
+ export interface RefreshedTokens {
19
+ readonly access: string;
20
+ readonly refresh: string;
21
+ /** Absolute expiry, epoch ms (with a 5-minute early-refresh skew baked in). */
22
+ readonly expires: number;
23
+ }
24
+ /** Refresh an expired (or near-expiry) access token. */
25
+ export declare const refreshAnthropicToken: (refreshToken: string) => Effect.Effect<RefreshedTokens, AuthError>;
26
+ export interface Pkce {
27
+ readonly verifier: string;
28
+ readonly challenge: string;
29
+ }
30
+ /** Generate a PKCE verifier + S256 challenge (Web Crypto; Bun-global). */
31
+ export declare const generatePkce: Effect.Effect<Pkce>;
32
+ export interface OAuthBegin {
33
+ /** The PKCE verifier — ALSO the `state`; the driver must verify the
34
+ * redirect echoes it (CSRF) before exchanging. */
35
+ readonly verifier: string;
36
+ readonly authorizeUrl: string;
37
+ readonly callbackPort: number;
38
+ readonly callbackPath: string;
39
+ }
40
+ /** Start an authorization: PKCE material + the browser URL. */
41
+ export declare const beginAnthropicOAuth: Effect.Effect<OAuthBegin>;
42
+ /** Exchange the authorization code for tokens. */
43
+ export declare const exchangeAnthropicCode: (code: string, verifier: string) => Effect.Effect<RefreshedTokens, AuthError>;
44
+ export interface AuthRedirect {
45
+ readonly code: Option.Option<string>;
46
+ readonly state: Option.Option<string>;
47
+ }
48
+ /** Pull `code`/`state` out of a pasted redirect URL, `code#state` pair,
49
+ * bare query string, or raw code. Total — garbage is `{none, none}`. */
50
+ export declare const parseAuthorizationInput: (input: string) => AuthRedirect;
@@ -0,0 +1,90 @@
1
+ import { Effect, Option } from "effect";
2
+ import { AuthError } from "@xandreed/core";
3
+ export const ANTHROPIC_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
4
+ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize", TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
5
+ export const ANTHROPIC_CALLBACK_PORT = 53692, ANTHROPIC_CALLBACK_PATH = "/callback", ANTHROPIC_REDIRECT_URI = `http://localhost:${ANTHROPIC_CALLBACK_PORT}${ANTHROPIC_CALLBACK_PATH}`;
6
+ const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
7
+ export const ANTHROPIC_OAUTH_BETA = "claude-code-20250219,oauth-2025-04-20", CLAUDE_CODE_SYSTEM = "You are Claude Code, Anthropic's official CLI for Claude.";
8
+ const oauthError = (stage) => (detail) => new AuthError({
9
+ provider: "anthropic",
10
+ message: `OAuth ${stage} failed: ${detail}`
11
+ }), postToken = (body, stage) => {
12
+ const fail = oauthError(stage);
13
+ return Effect.tryPromise({
14
+ try: async () => {
15
+ const res = await fetch(TOKEN_URL, {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
18
+ body: JSON.stringify(body),
19
+ signal: AbortSignal.timeout(30000)
20
+ });
21
+ return { ok: res.ok, status: res.status, text: await res.text() };
22
+ },
23
+ catch: (e) => fail(String(e))
24
+ }).pipe(Effect.filterOrFail((res) => res.ok, (res) => fail(`status ${res.status}: ${res.text.slice(0, 200)}`)), Effect.flatMap((res) => Effect.try({
25
+ try: () => JSON.parse(res.text),
26
+ catch: () => fail("the token response was not JSON")
27
+ })), Effect.map((data) => ({
28
+ access: data.access_token,
29
+ refresh: data.refresh_token,
30
+ expires: Date.now() + data.expires_in * 1000 - 300000
31
+ })));
32
+ };
33
+ export const refreshAnthropicToken = (refreshToken) => postToken({
34
+ grant_type: "refresh_token",
35
+ client_id: ANTHROPIC_CLIENT_ID,
36
+ refresh_token: refreshToken
37
+ }, "token refresh");
38
+ const base64url = (bytes) => btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
39
+ export const generatePkce = Effect.promise(async () => {
40
+ const verifierBytes = new Uint8Array(32);
41
+ crypto.getRandomValues(verifierBytes);
42
+ const verifier = base64url(verifierBytes), digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
43
+ return { verifier, challenge: base64url(new Uint8Array(digest)) };
44
+ }), beginAnthropicOAuth = Effect.map(generatePkce, (pkce) => ({
45
+ verifier: pkce.verifier,
46
+ authorizeUrl: `${AUTHORIZE_URL}?${new URLSearchParams({
47
+ code: "true",
48
+ client_id: ANTHROPIC_CLIENT_ID,
49
+ response_type: "code",
50
+ redirect_uri: ANTHROPIC_REDIRECT_URI,
51
+ scope: SCOPES,
52
+ code_challenge: pkce.challenge,
53
+ code_challenge_method: "S256",
54
+ state: pkce.verifier
55
+ }).toString()}`,
56
+ callbackPort: ANTHROPIC_CALLBACK_PORT,
57
+ callbackPath: ANTHROPIC_CALLBACK_PATH
58
+ })), exchangeAnthropicCode = (code, verifier) => postToken({
59
+ grant_type: "authorization_code",
60
+ client_id: ANTHROPIC_CLIENT_ID,
61
+ code,
62
+ redirect_uri: ANTHROPIC_REDIRECT_URI,
63
+ code_verifier: verifier,
64
+ state: verifier
65
+ }, "code exchange");
66
+ const fromNullableTrimmed = (value) => Option.filter(Option.fromNullable(value), (v) => v.trim().length > 0);
67
+ export const parseAuthorizationInput = (input) => {
68
+ const value = input.trim();
69
+ if (value.length === 0)
70
+ return { code: Option.none(), state: Option.none() };
71
+ if (URL.canParse(value)) {
72
+ const url = new URL(value);
73
+ return {
74
+ code: fromNullableTrimmed(url.searchParams.get("code")),
75
+ state: fromNullableTrimmed(url.searchParams.get("state"))
76
+ };
77
+ }
78
+ if (value.includes("#")) {
79
+ const [code, state] = value.split("#", 2);
80
+ return { code: fromNullableTrimmed(code), state: fromNullableTrimmed(state) };
81
+ }
82
+ if (value.includes("code=")) {
83
+ const params = new URLSearchParams(value);
84
+ return {
85
+ code: fromNullableTrimmed(params.get("code")),
86
+ state: fromNullableTrimmed(params.get("state"))
87
+ };
88
+ }
89
+ return { code: Option.some(value), state: Option.none() };
90
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { Layer } from "effect";
2
+ import { AuthStore } from "@xandreed/core";
3
+ export declare const authPaths: (cwd: string, home: string, directory?: string) => ReadonlyArray<string>;
4
+ /**
5
+ * `LocalAuthStoreLive(cwd, home)` — read-through: every call re-reads disk,
6
+ * so a credential added mid-session applies on the next request.
7
+ */
8
+ export declare const LocalAuthStoreLive: (cwd: string, home: string, directory?: string, fallbackDirectory?: string) => Layer.Layer<AuthStore, never, never>;
@@ -0,0 +1,90 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { Effect, Layer, Option, Redacted } from "effect";
4
+ import { asJsonRecord, AuthError, AuthStore, parseJsonWarn } from "@xandreed/core";
5
+ import { refreshAnthropicToken } from "./anthropicOAuth.js";
6
+ import { refreshOpenAiCodexToken } from "./openAiCodexOAuth.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
+ }), REFRESH_SKEW_MS = 60000;
46
+ export const authPaths = (cwd, home, directory = ".efferent") => [
47
+ join(home, directory, "auth.json"),
48
+ 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)];
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.fromNullable(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
+ });
90
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { Effect, Option } from "effect";
2
+ import { AuthError } from "@xandreed/core";
3
+ /** ChatGPT Plus/Pro OAuth protocol used by the OpenAI Codex subscription. */
4
+ export declare const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
5
+ export declare const OPENAI_CODEX_CALLBACK_PORT = 1455;
6
+ export declare const OPENAI_CODEX_CALLBACK_PATH = "/auth/callback";
7
+ export declare const OPENAI_CODEX_REDIRECT_URI = "http://localhost:1455/auth/callback";
8
+ export interface OpenAiCodexTokens {
9
+ readonly access: string;
10
+ readonly refresh: string;
11
+ readonly expires: number;
12
+ readonly accountId?: string;
13
+ }
14
+ export interface OpenAiCodexOAuthBegin {
15
+ readonly verifier: string;
16
+ readonly state: string;
17
+ readonly authorizeUrl: string;
18
+ readonly callbackPort: number;
19
+ readonly callbackPath: string;
20
+ }
21
+ /** Extract the subscription account lane without exposing the token. */
22
+ export declare const openAiCodexAccountId: (accessToken: string) => Option.Option<string>;
23
+ 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>;
@@ -0,0 +1,58 @@
1
+ import { Effect, Either, Option } from "effect";
2
+ import { AuthError } from "@xandreed/core";
3
+ import { generatePkce } from "./anthropicOAuth.js";
4
+ export const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
5
+ const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize", TOKEN_URL = "https://auth.openai.com/oauth/token";
6
+ 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}`;
7
+ 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) => {
8
+ const normalized = segment.replace(/-/g, "+").replace(/_/g, "/"), padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
9
+ return JSON.parse(atob(padded));
10
+ };
11
+ export const openAiCodexAccountId = (accessToken) => {
12
+ const segment = accessToken.split(".")[1];
13
+ return Option.flatMap(Option.fromNullable(segment), (encoded) => Option.flatMap(Either.getRight(Either.try(() => base64UrlJson(encoded))), (payload) => {
14
+ const claim = payload[ACCOUNT_CLAIM];
15
+ if (typeof claim !== "object" || claim === null)
16
+ return Option.none();
17
+ const accountId = claim.chatgpt_account_id;
18
+ return Option.filter(Option.fromNullable(typeof accountId === "string" ? accountId : void 0), (value) => value.length > 0);
19
+ }));
20
+ };
21
+ const postToken = (body, stage) => Effect.tryPromise({
22
+ try: async () => {
23
+ const response = await fetch(TOKEN_URL, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
26
+ body: new URLSearchParams(body),
27
+ signal: AbortSignal.timeout(30000)
28
+ });
29
+ return { ok: response.ok, status: response.status, text: await response.text() };
30
+ },
31
+ catch: (error) => oauthError(stage, String(error))
32
+ }).pipe(Effect.filterOrFail((response) => response.ok, (response) => oauthError(stage, `status ${response.status}: ${response.text.slice(0, 200)}`)), Effect.flatMap((response) => Effect.try({
33
+ try: () => JSON.parse(response.text),
34
+ 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
+ access: token.access_token,
37
+ refresh: token.refresh_token,
38
+ expires: Date.now() + token.expires_in * 1000,
39
+ ...Option.match(openAiCodexAccountId(token.access_token), { onNone: () => ({}), onSome: (accountId) => ({ accountId }) })
40
+ })));
41
+ export const beginOpenAiCodexOAuth = Effect.gen(function* () {
42
+ const pkce = yield* generatePkce, stateBytes = new Uint8Array(16);
43
+ crypto.getRandomValues(stateBytes);
44
+ const state = Array.from(stateBytes, (byte) => byte.toString(16).padStart(2, "0")).join(""), url = new URL(AUTHORIZE_URL);
45
+ Object.entries({
46
+ response_type: "code",
47
+ client_id: OPENAI_CODEX_CLIENT_ID,
48
+ redirect_uri: OPENAI_CODEX_REDIRECT_URI,
49
+ scope: SCOPE,
50
+ code_challenge: pkce.challenge,
51
+ code_challenge_method: "S256",
52
+ state,
53
+ id_token_add_organizations: "true",
54
+ codex_cli_simplified_flow: "true",
55
+ originator: "pi"
56
+ }).forEach(([key, value]) => url.searchParams.set(key, value));
57
+ 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");
@@ -0,0 +1,19 @@
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";
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
+ export type { OpenAiCodexOAuthBegin, OpenAiCodexTokens } from "./auth/openAiCodexOAuth.js";
5
+ export { authPaths, LocalAuthStoreLive } from "./auth/localAuth.js";
6
+ export { LocalSettingsStoreLive } from "./settings/localSettings.js";
7
+ export { roleModelView } from "./settings/roleView.js";
8
+ export { fromChatCompletion, makeCompatLanguageModel, toChatMessages, toChatTools, } from "./llm/compat.js";
9
+ export type { CompatConfig } from "./llm/compat.js";
10
+ export { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL, toOpenAiCodexRequestBody } from "./llm/openAiCodex.js";
11
+ export { normalizeOpenAiCodexWebSocketEvent, openAiCodexUuidV7, OpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
12
+ export { buildProvider, OPENCODE_CHAT_URL, OPENCODE_RESPONSES_API_URL, prependClaudeCode, usesOpenCodeResponses, withAnthropicCacheBreakpoints, } from "./llm/providers.js";
13
+ export { classifyLlmError, LLM_REQUEST_TIMEOUT_MS, MAX_HONORED_RETRY_AFTER_MS, rejectEmptyResponse, retryableLlm, } from "./llm/retry.js";
14
+ export type { ErrorClass } from "./llm/retry.js";
15
+ export { generateWith, LanguageModelLive, LanguageModelSelectionLive } from "./llm/router.js";
16
+ export { configuredModelCatalog, ConfiguredModelCatalogLive, reasoningEffortsFor, } from "./llm/modelCatalog.js";
17
+ export type { ReasoningEffort } from "./llm/modelCatalog.js";
18
+ export { UtilityLlmLive } from "./llm/utilityLlm.js";
19
+ export { modelsPlugin, default } from "./plugin.adapter.js";
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ export {
2
+ ANTHROPIC_OAUTH_BETA,
3
+ beginAnthropicOAuth,
4
+ CLAUDE_CODE_SYSTEM,
5
+ exchangeAnthropicCode,
6
+ generatePkce,
7
+ parseAuthorizationInput,
8
+ refreshAnthropicToken
9
+ } from "./auth/anthropicOAuth.js";
10
+ export {
11
+ beginOpenAiCodexOAuth,
12
+ exchangeOpenAiCodexCode,
13
+ OPENAI_CODEX_CALLBACK_PATH,
14
+ OPENAI_CODEX_CALLBACK_PORT,
15
+ OPENAI_CODEX_CLIENT_ID,
16
+ OPENAI_CODEX_REDIRECT_URI,
17
+ openAiCodexAccountId,
18
+ refreshOpenAiCodexToken
19
+ } from "./auth/openAiCodexOAuth.js";
20
+ export { authPaths, LocalAuthStoreLive } from "./auth/localAuth.js";
21
+ export { LocalSettingsStoreLive } from "./settings/localSettings.js";
22
+ export { roleModelView } from "./settings/roleView.js";
23
+ export {
24
+ fromChatCompletion,
25
+ makeCompatLanguageModel,
26
+ toChatMessages,
27
+ toChatTools
28
+ } from "./llm/compat.js";
29
+ export { makeOpenAiCodexLanguageModel, OPENAI_CODEX_API_URL, toOpenAiCodexRequestBody } from "./llm/openAiCodex.js";
30
+ export { normalizeOpenAiCodexWebSocketEvent, openAiCodexUuidV7, OpenAiCodexWebSocketHttpClient } from "./llm/openAiCodexWebSocket.js";
31
+ export {
32
+ buildProvider,
33
+ OPENCODE_CHAT_URL,
34
+ OPENCODE_RESPONSES_API_URL,
35
+ prependClaudeCode,
36
+ usesOpenCodeResponses,
37
+ withAnthropicCacheBreakpoints
38
+ } from "./llm/providers.js";
39
+ export {
40
+ classifyLlmError,
41
+ LLM_REQUEST_TIMEOUT_MS,
42
+ MAX_HONORED_RETRY_AFTER_MS,
43
+ rejectEmptyResponse,
44
+ retryableLlm
45
+ } from "./llm/retry.js";
46
+ export { generateWith, LanguageModelLive, LanguageModelSelectionLive } from "./llm/router.js";
47
+ export {
48
+ configuredModelCatalog,
49
+ ConfiguredModelCatalogLive,
50
+ reasoningEffortsFor
51
+ } from "./llm/modelCatalog.js";
52
+ export { UtilityLlmLive } from "./llm/utilityLlm.js";
53
+ export { modelsPlugin, default } from "./plugin.adapter.js";
@@ -0,0 +1,77 @@
1
+ import { AiError, LanguageModel, Tool } from "@effect/ai";
2
+ import type { Prompt } from "@effect/ai";
3
+ import { Effect } from "effect";
4
+ import type { CompletionUsage } from "./sse.js";
5
+ /**
6
+ * A generic OpenAI-compatible `/chat/completions` `LanguageModel` over raw
7
+ * `fetch`. The official `@effect/ai-openai` client targets api.openai.com;
8
+ * gateways like OpenCode's speak the same protocol at a different base URL
9
+ * with a Bearer key, so this client is parameterized by `chatUrl` + `apiKey`.
10
+ *
11
+ * `generateText` consumes whole turns (`stream: false`); `streamText` runs
12
+ * the same request with `stream: true` through the pure SSE state machine in
13
+ * `sse.ts`. Errors BEFORE any stream part surface with the same taxonomy as
14
+ * `generateText` — that boundary is what the retry gate (router) keys on.
15
+ */
16
+ type Json = Record<string, unknown>;
17
+ export interface CompatConfig {
18
+ /** Module name for `AiError` provenance (e.g. "OpenCode"). */
19
+ readonly moduleName: string;
20
+ /** Full chat-completions endpoint. */
21
+ readonly chatUrl: string;
22
+ /** Bearer key (already resolved from the AuthStore). */
23
+ readonly apiKey: string;
24
+ /** Provider-native model id. */
25
+ readonly model: string;
26
+ /** Host-selected thinking mode. Omit to retain family defaults. */
27
+ readonly thinking?: "enabled" | "disabled";
28
+ /** Gateway reasoning vocabulary, independent of provider-native thinking. */
29
+ readonly reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
30
+ readonly temperature?: number;
31
+ /** Injectable for tests; defaults to global fetch. */
32
+ readonly fetchImpl?: typeof fetch;
33
+ /** Host-owned admission (e.g. reserve a worst-case cost) before every call. */
34
+ readonly beforeRequest?: (body: Readonly<Record<string, unknown>>) => Effect.Effect<void, AiError.AiError>;
35
+ /** Per-call accounting/resource boundary. Non-streaming responses are fully
36
+ * buffered inside request so interruption also aborts body consumption. */
37
+ readonly aroundRequest?: (body: Readonly<Record<string, unknown>>, request: Effect.Effect<Response, AiError.AiError>) => Effect.Effect<Response, AiError.AiError>;
38
+ }
39
+ /** Decoded `Prompt` messages → chat-completions message objects. */
40
+ export declare const toChatMessages: (prompt: Prompt.Prompt) => ReadonlyArray<Json>;
41
+ /** User-defined tools → chat-completions function declarations (empty when
42
+ * the request carries none — the caller omits the `tools` key entirely). */
43
+ export declare const toChatTools: (tools: ReadonlyArray<Tool.Any>) => ReadonlyArray<Json>;
44
+ /**
45
+ * FORCE thinking on the families that think adaptively. Live forensics
46
+ * (2026-07-09): kimi-k2.7-code skipped thinking on 24 turns of one forge run
47
+ * and emitted degenerate skeleton tool calls (write_file with an EMPTY body,
48
+ * 24 output tokens, finish "tool-calls") on exactly those turns — the
49
+ * no-think ↔ empty-call correlation was 24/24. Deepseek-style families take
50
+ * `thinking: {type: "enabled"}`; qwen takes `enable_thinking`. Disabling is
51
+ * never sent (kimi-k2.7+ rejects it outright); unknown families get nothing.
52
+ */
53
+ export declare const thinkingParams: (model: string) => Record<string, unknown>;
54
+ interface ChatCompletion {
55
+ readonly choices?: ReadonlyArray<{
56
+ readonly finish_reason?: string;
57
+ readonly message?: {
58
+ readonly content?: string | null;
59
+ /** DeepSeek-native vocabulary (kimi-k2.7-code, deepseek serve this). */
60
+ readonly reasoning_content?: string | null;
61
+ /** OpenRouter-style vocabulary (kimi-k2.6 via Moonshot serves this). */
62
+ readonly reasoning?: string | null;
63
+ readonly tool_calls?: ReadonlyArray<{
64
+ readonly id?: string;
65
+ readonly function?: {
66
+ readonly name?: string;
67
+ readonly arguments?: string;
68
+ };
69
+ }>;
70
+ };
71
+ }>;
72
+ readonly usage?: CompletionUsage;
73
+ }
74
+ /** Parse one non-streaming completion into `@effect/ai` encoded parts. */
75
+ export declare const fromChatCompletion: (moduleName: string, body: ChatCompletion) => Effect.Effect<ReadonlyArray<unknown>, AiError.AiError>;
76
+ export declare const makeCompatLanguageModel: (config: CompatConfig) => Effect.Effect<LanguageModel.Service>;
77
+ export {};