@velum-labs/routekit-daemon 1.3.2 → 1.5.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 (172) hide show
  1. package/dist/account-enroll-service.d.ts +1 -3
  2. package/dist/account-enroll-service.js +262 -262
  3. package/dist/account-mutation-service.d.ts +1 -3
  4. package/dist/account-mutation-service.js +302 -283
  5. package/dist/account-query-service.d.ts +1 -3
  6. package/dist/account-query-service.js +88 -86
  7. package/dist/call-attribution-store.js +544 -1
  8. package/dist/control-dispatch.js +9 -15
  9. package/dist/daemon-bootstrap-preflight.js +1 -1
  10. package/dist/daemon-control-handlers.d.ts +1 -1
  11. package/dist/daemon-control-handlers.js +24 -27
  12. package/dist/daemon-generations.d.ts +14 -28
  13. package/dist/daemon-generations.js +114 -53
  14. package/dist/daemon-lifecycle-service.d.ts +1 -3
  15. package/dist/daemon-lifecycle-service.js +88 -94
  16. package/dist/daemon-maintenance.d.ts +27 -49
  17. package/dist/daemon-maintenance.js +5 -1
  18. package/dist/daemon-options.d.ts +3 -2
  19. package/dist/daemon-policy-context.d.ts +1 -0
  20. package/dist/daemon-policy-context.js +1 -0
  21. package/dist/daemon-runtime-state.d.ts +1 -1
  22. package/dist/daemon-runtime-state.js +2 -3
  23. package/dist/daemon-state-context.d.ts +1 -1
  24. package/dist/daemon-state-context.js +1 -38
  25. package/dist/doctor-service.d.ts +1 -3
  26. package/dist/doctor-service.js +87 -91
  27. package/dist/effect/daemon-live.d.ts +7 -6
  28. package/dist/effect/daemon-live.js +57 -135
  29. package/dist/eval-routing-service.d.ts +1 -3
  30. package/dist/eval-routing-service.js +45 -69
  31. package/dist/eval-session-execution-store.d.ts +24 -0
  32. package/dist/eval-session-execution-store.js +253 -0
  33. package/dist/host-protocol.d.ts +1 -0
  34. package/dist/host.js +3 -0
  35. package/dist/internal-providers/provider-anthropic/index.d.ts +23 -0
  36. package/dist/internal-providers/provider-anthropic/index.js +42 -0
  37. package/dist/internal-providers/provider-anthropic/test/provider.test.js +33 -0
  38. package/dist/internal-providers/provider-azure/index.d.ts +25 -0
  39. package/dist/internal-providers/provider-azure/index.js +101 -0
  40. package/dist/internal-providers/provider-azure/test/provider.test.js +83 -0
  41. package/dist/internal-providers/provider-bedrock/codec.d.ts +11 -0
  42. package/dist/internal-providers/provider-bedrock/codec.js +484 -0
  43. package/dist/internal-providers/provider-bedrock/connection-settings.d.ts +6 -0
  44. package/dist/internal-providers/provider-bedrock/connection-settings.js +41 -0
  45. package/dist/internal-providers/provider-bedrock/connection.d.ts +17 -0
  46. package/dist/internal-providers/provider-bedrock/connection.js +143 -0
  47. package/dist/internal-providers/provider-bedrock/index.d.ts +27 -0
  48. package/dist/internal-providers/provider-bedrock/index.js +47 -0
  49. package/dist/internal-providers/provider-bedrock/source.d.ts +39 -0
  50. package/dist/internal-providers/provider-bedrock/source.js +454 -0
  51. package/dist/internal-providers/provider-bedrock/test/bedrock-source.test.d.ts +1 -0
  52. package/dist/internal-providers/provider-bedrock/test/bedrock-source.test.js +786 -0
  53. package/dist/internal-providers/provider-bedrock/test/connection.test.d.ts +1 -0
  54. package/dist/internal-providers/provider-bedrock/test/connection.test.js +249 -0
  55. package/dist/internal-providers/provider-bedrock/test/workload-credentials.test.d.ts +1 -0
  56. package/dist/internal-providers/provider-bedrock/test/workload-credentials.test.js +29 -0
  57. package/dist/internal-providers/provider-bedrock/workload-credentials.d.ts +5 -0
  58. package/dist/internal-providers/provider-bedrock/workload-credentials.js +14 -0
  59. package/dist/internal-providers/provider-claude-code/index.d.ts +6 -0
  60. package/dist/internal-providers/provider-claude-code/index.js +4 -0
  61. package/dist/internal-providers/provider-claude-code/module-api.d.ts +25 -0
  62. package/dist/internal-providers/provider-claude-code/module-api.js +31 -0
  63. package/dist/internal-providers/provider-claude-code/relay.d.ts +20 -0
  64. package/dist/internal-providers/provider-claude-code/relay.js +115 -0
  65. package/dist/internal-providers/provider-claude-code/subscription.d.ts +2 -0
  66. package/dist/internal-providers/provider-claude-code/subscription.js +194 -0
  67. package/dist/internal-providers/provider-claude-code/test/http-submission.test.d.ts +1 -0
  68. package/dist/internal-providers/provider-claude-code/test/http-submission.test.js +140 -0
  69. package/dist/internal-providers/provider-claude-code/test/relay.test.d.ts +1 -0
  70. package/dist/internal-providers/provider-claude-code/test/relay.test.js +379 -0
  71. package/dist/internal-providers/provider-claude-code/test/subscription.test.d.ts +1 -0
  72. package/dist/internal-providers/provider-claude-code/test/subscription.test.js +256 -0
  73. package/dist/internal-providers/provider-cliproxy/index.d.ts +9 -0
  74. package/dist/internal-providers/provider-cliproxy/index.js +15 -0
  75. package/dist/internal-providers/provider-codex/index.d.ts +6 -0
  76. package/dist/internal-providers/provider-codex/index.js +7 -0
  77. package/dist/internal-providers/provider-codex/module-api.d.ts +25 -0
  78. package/dist/internal-providers/provider-codex/module-api.js +31 -0
  79. package/dist/internal-providers/provider-codex/relay.d.ts +123 -0
  80. package/dist/internal-providers/provider-codex/relay.js +261 -0
  81. package/dist/internal-providers/provider-codex/subscription.d.ts +2 -0
  82. package/dist/internal-providers/provider-codex/subscription.js +555 -0
  83. package/dist/internal-providers/provider-codex/test/http-submission.test.d.ts +1 -0
  84. package/dist/internal-providers/provider-codex/test/http-submission.test.js +142 -0
  85. package/dist/internal-providers/provider-codex/test/relay.test.d.ts +1 -0
  86. package/dist/internal-providers/provider-codex/test/relay.test.js +600 -0
  87. package/dist/internal-providers/provider-codex/test/subscription.test.d.ts +1 -0
  88. package/dist/internal-providers/provider-codex/test/subscription.test.js +70 -0
  89. package/dist/internal-providers/provider-codex/test/translated-content.test.d.ts +1 -0
  90. package/dist/internal-providers/provider-codex/test/translated-content.test.js +91 -0
  91. package/dist/internal-providers/provider-custom/index.d.ts +9 -0
  92. package/dist/internal-providers/provider-custom/index.js +15 -0
  93. package/dist/internal-providers/provider-google/index.d.ts +23 -0
  94. package/dist/internal-providers/provider-google/index.js +43 -0
  95. package/dist/internal-providers/provider-google/test/provider.test.d.ts +1 -0
  96. package/dist/internal-providers/provider-google/test/provider.test.js +34 -0
  97. package/dist/internal-providers/provider-google-vertex/index.d.ts +28 -0
  98. package/dist/internal-providers/provider-google-vertex/index.js +137 -0
  99. package/dist/internal-providers/provider-google-vertex/test/provider.test.d.ts +1 -0
  100. package/dist/internal-providers/provider-google-vertex/test/provider.test.js +85 -0
  101. package/dist/internal-providers/provider-openai/documented-reasoning.d.ts +9 -0
  102. package/dist/internal-providers/provider-openai/documented-reasoning.js +26 -0
  103. package/dist/internal-providers/provider-openai/index.d.ts +23 -0
  104. package/dist/internal-providers/provider-openai/index.js +74 -0
  105. package/dist/internal-providers/provider-openai/test/provider.test.d.ts +1 -0
  106. package/dist/internal-providers/provider-openai/test/provider.test.js +268 -0
  107. package/dist/internal-providers/provider-openai-compatible/index.d.ts +9 -0
  108. package/dist/internal-providers/provider-openai-compatible/index.js +15 -0
  109. package/dist/internal-providers/provider-openai-compatible/test/provider.test.d.ts +1 -0
  110. package/dist/internal-providers/provider-openai-compatible/test/provider.test.js +40 -0
  111. package/dist/internal-providers/provider-openrouter/connection.d.ts +5 -0
  112. package/dist/internal-providers/provider-openrouter/connection.js +143 -0
  113. package/dist/internal-providers/provider-openrouter/index.d.ts +28 -0
  114. package/dist/internal-providers/provider-openrouter/index.js +42 -0
  115. package/dist/internal-providers/provider-openrouter/pricing.d.ts +4 -0
  116. package/dist/internal-providers/provider-openrouter/pricing.js +108 -0
  117. package/dist/internal-providers/provider-openrouter/test/connection.test.d.ts +1 -0
  118. package/dist/internal-providers/provider-openrouter/test/connection.test.js +71 -0
  119. package/dist/internal-providers/provider-openrouter/test/pricing.test.d.ts +1 -0
  120. package/dist/internal-providers/provider-openrouter/test/pricing.test.js +45 -0
  121. package/dist/internal-providers/provider-openrouter/test/provider.test.d.ts +1 -0
  122. package/dist/internal-providers/provider-openrouter/test/provider.test.js +141 -0
  123. package/dist/internal-providers/provider-openrouter/test/validation.test.d.ts +1 -0
  124. package/dist/internal-providers/provider-openrouter/test/validation.test.js +225 -0
  125. package/dist/launcher-service.d.ts +3 -7
  126. package/dist/launcher-service.js +144 -133
  127. package/dist/provider-modules.d.ts +6 -0
  128. package/dist/provider-modules.js +55 -0
  129. package/dist/provider-query-service.d.ts +1 -3
  130. package/dist/provider-query-service.js +140 -143
  131. package/dist/router-generation-service.d.ts +1 -3
  132. package/dist/router-generation-service.js +83 -78
  133. package/dist/services/eval-session/service.d.ts +20 -8
  134. package/dist/services/eval-session/service.js +410 -40
  135. package/dist/services/gateway-generation/service.d.ts +7 -3
  136. package/dist/services/gateway-generation/service.js +126 -63
  137. package/dist/services/generations/service.d.ts +3 -3
  138. package/dist/services/generations/service.js +5 -1
  139. package/dist/telemetry-application-service.d.ts +1 -3
  140. package/dist/telemetry-application-service.js +41 -45
  141. package/dist/test/call-attribution-store.test.js +186 -0
  142. package/dist/test/daemon-accounts.test.js +2 -2
  143. package/dist/test/daemon-data-plane.test.js +24 -45
  144. package/dist/test/daemon-fixtures.js +1 -1
  145. package/dist/test/daemon-generation-transaction.test.js +66 -2
  146. package/dist/test/daemon-runtime-state.test.js +28 -1
  147. package/dist/test/effect-daemon.test.js +38 -0
  148. package/dist/test/eval-routing-application-service.test.js +6 -6
  149. package/dist/test/eval-session-execution.test.d.ts +1 -0
  150. package/dist/test/eval-session-execution.test.js +356 -0
  151. package/dist/test/eval-session-service.test.js +70 -1
  152. package/dist/test/host-worker-session.test.js +1 -0
  153. package/dist/test/launcher-session.test.d.ts +1 -0
  154. package/dist/test/launcher-session.test.js +87 -0
  155. package/dist/test/leaderboard.test.js +25 -1
  156. package/dist/test/provider-modules.test.d.ts +1 -0
  157. package/dist/test/provider-modules.test.js +65 -0
  158. package/dist/test/routekit-upstream-sources.test.d.ts +1 -0
  159. package/dist/test/routekit-upstream-sources.test.js +40 -0
  160. package/dist/test/runtime-composition-types.test.js +11 -0
  161. package/dist/token-application-service.d.ts +1 -3
  162. package/dist/token-application-service.js +38 -44
  163. package/dist/worker.js +5 -3
  164. package/package.json +35 -11
  165. package/dist/application-services.d.ts +0 -10
  166. package/dist/application-services.js +0 -22
  167. package/dist/daemon-lifecycle.d.ts +0 -54
  168. package/dist/daemon-lifecycle.js +0 -177
  169. package/dist/test/application-services.test.js +0 -135
  170. package/dist/test/daemon-lifecycle.test.js +0 -182
  171. /package/dist/{test/application-services.test.d.ts → internal-providers/provider-anthropic/test/provider.test.d.ts} +0 -0
  172. /package/dist/{test/daemon-lifecycle.test.d.ts → internal-providers/provider-azure/test/provider.test.d.ts} +0 -0
@@ -0,0 +1,7 @@
1
+ import { CodexBackendRelay } from "./relay.js";
2
+ export { codex, codexProvider, codexSubscriptionProvider } from "./module-api.js";
3
+ export { CodexBackendRelay, codexRelayAuth } from "./relay.js";
4
+ export const codexRelayFactory = (accounts) => new CodexBackendRelay({
5
+ catalog: (_template, stock) => [...stock],
6
+ auth: { kind: "accounts", accounts }
7
+ });
@@ -0,0 +1,25 @@
1
+ import { SubscriptionAccountBackend } from "@velum-labs/routekit-accounts/provider";
2
+ import { Effect } from "effect";
3
+ export declare const codex: {
4
+ readonly id: "codex";
5
+ readonly protocol: "codex";
6
+ readonly accountClass: "subscription";
7
+ readonly billingMode: "subscription";
8
+ readonly connection: Readonly<{
9
+ validate?: (context: Readonly<{
10
+ sourceId: string;
11
+ config: import("@velum-labs/routekit-config-core").SourceConfig;
12
+ env: Readonly<Record<string, string | undefined>>;
13
+ resources?: Readonly<Record<string, unknown>>;
14
+ }>) => Effect.Effect<void, Error, import("@velum-labs/routekit-runtime/effect").RouteKitPlatform>;
15
+ credentialValidation?: "registry-key-probe" | "registry-key-probe-if-configured-models";
16
+ manifest: import("@velum-labs/routekit-provider-core").ProviderConnectionManifest;
17
+ prepare?: (input: import("@velum-labs/routekit-provider-core").PrepareProviderConnectionInput) => import("@velum-labs/routekit-provider-core").PreparedProviderConnection;
18
+ enrollment?: Readonly<Record<string, import("@velum-labs/routekit-provider-core").ProviderEnrollmentAdapter>>;
19
+ credentials?: import("@velum-labs/routekit-provider-core").ProviderCredentialAdapter;
20
+ subscriptionMode?: import("@velum-labs/routekit-registry").SubscriptionMode;
21
+ }>;
22
+ readonly createSource: (context: import("@velum-labs/routekit-provider-core").ProviderModuleContext) => Effect.Effect<SubscriptionAccountBackend, never, never>;
23
+ };
24
+ export { codexProvider } from "./subscription.js";
25
+ export declare const codexSubscriptionProvider: import("@velum-labs/routekit-accounts").SubscriptionProvider<"codex">;
@@ -0,0 +1,31 @@
1
+ import { SubscriptionAccountBackend } from "@velum-labs/routekit-accounts/provider";
2
+ import { CodexResponsesBackend, defineProviderConnection, defineProvider } from "@velum-labs/routekit-provider-core";
3
+ import { Effect } from "effect";
4
+ import { codexProvider } from "./subscription.js";
5
+ export const codex = defineProvider({
6
+ id: "codex",
7
+ protocol: "codex",
8
+ accountClass: "subscription",
9
+ billingMode: "subscription",
10
+ connection: defineProviderConnection({
11
+ subscriptionMode: "codex",
12
+ manifest: {
13
+ version: 1, id: "codex", displayName: "ChatGPT Codex", category: "subscription",
14
+ description: "Connect a ChatGPT subscription through the Codex OAuth flow.",
15
+ methods: [{ id: "oauth", kind: "oauth", label: "Sign in with ChatGPT", schemaRevision: 1, fields: [] }]
16
+ }
17
+ }),
18
+ createSource: (context) => Effect.sync(() => {
19
+ const accountSet = context.resources?.accountSet;
20
+ if (accountSet === undefined || accountSet.mode !== "codex") {
21
+ throw new Error('provider module "codex" requires its daemon-owned account set');
22
+ }
23
+ return new SubscriptionAccountBackend({
24
+ sourceId: context.sourceId,
25
+ accountSet,
26
+ backendFactory: (_mode, backendOptions) => new CodexResponsesBackend(backendOptions)
27
+ });
28
+ })
29
+ });
30
+ export { codexProvider } from "./subscription.js";
31
+ export const codexSubscriptionProvider = codexProvider();
@@ -0,0 +1,123 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+ import type { SubscriptionAccountSet, SubscriptionAccountSetSnapshot, SubscriptionRelay } from "@velum-labs/routekit-accounts";
3
+ import { RouteKitFailure } from "@velum-labs/routekit-runtime/effect";
4
+ import { Effect } from "effect";
5
+ import { z } from "zod";
6
+ /**
7
+ * The Codex backend relay: lets a Codex client keep its own stock models while
8
+ * pointed at a RouteKit gateway as its model provider.
9
+ *
10
+ * The relay supports two explicit credential trust models:
11
+ *
12
+ * - `client` forwards the ChatGPT login Codex attached to the request.
13
+ * - `accounts` selects from a server-owned {@link SubscriptionAccountSet}.
14
+ *
15
+ * In either mode:
16
+ *
17
+ * - `GET /v1/models`: forwarded to the ChatGPT Codex backend with the client's
18
+ * relayed auth, and the live stock catalog is merged behind the configured
19
+ * entries, so the picker is always the union of what the router adds and
20
+ * what Codex would natively offer. When the upstream fetch is impossible
21
+ * (no relayable auth, offline), the merge falls back to the locally cached
22
+ * stock catalog snapshot.
23
+ * - `POST /v1/responses` for a model the gateway does not serve locally:
24
+ * forwarded to the ChatGPT Codex backend with public string input normalized
25
+ * to the backend's canonical item-list form. Native item-list requests and
26
+ * auth remain untouched.
27
+ *
28
+ * Client mode never stores or mints credentials. Account-set mode strips
29
+ * ingress auth and injects the selected managed credential.
30
+ */
31
+ /** One model catalog entry in Codex's own `ModelInfo` wire shape. */
32
+ export type CodexCatalogEntry = Record<string, unknown>;
33
+ export type ProviderRelayLogger = {
34
+ warn(message: string): void;
35
+ error(message: string): void;
36
+ };
37
+ declare const stockEntrySchema: z.ZodObject<{
38
+ slug: z.ZodString;
39
+ }, z.core.$loose>;
40
+ /** A validated upstream stock entry: a known `slug`, everything else opaque. */
41
+ export type CodexStockEntry = z.infer<typeof stockEntrySchema>;
42
+ export type CodexRelayOptions = {
43
+ /**
44
+ * ChatGPT Codex backend base URL (no trailing slash). Defaults to the
45
+ * provider registry's `codex` base; override for tests/self-hosted proxies.
46
+ */
47
+ backendUrl?: string;
48
+ /**
49
+ * Build the full picker catalog: configured entries first, then the given
50
+ * stock entries merged behind them (deduped by slug).
51
+ */
52
+ catalog: (template: CodexCatalogEntry, stock: readonly CodexCatalogEntry[]) => CodexCatalogEntry[];
53
+ /**
54
+ * Stock catalog snapshot used when the upstream fetch is impossible
55
+ * (typically the user's `~/.codex/models_cache.json`).
56
+ */
57
+ fallbackStock?: () => CodexCatalogEntry[];
58
+ /** Upstream fetch timeout (ms). Codex itself caps the whole refresh at 5s. */
59
+ timeoutMs?: number;
60
+ logger?: ProviderRelayLogger;
61
+ /** Credential trust model. Defaults to forwarding the Codex client's auth. */
62
+ auth?: CodexRelayAuthSource;
63
+ };
64
+ export type CodexRelayAuthSource = {
65
+ kind: "client";
66
+ } | {
67
+ kind: "accounts";
68
+ accounts: SubscriptionAccountSet;
69
+ };
70
+ /** ChatGPT auth material relayed from the client's own request. */
71
+ export type CodexRelayAuth = {
72
+ authorization: string;
73
+ accountId: string;
74
+ };
75
+ /**
76
+ * Extract relayable ChatGPT auth from a request. Codex's ChatGPT-login auth
77
+ * provider always sends the bearer token together with a `chatgpt-account-id`
78
+ * header; the pair is what distinguishes it from a plain API key or a gateway
79
+ * bearer token, so only that combination is ever relayed.
80
+ */
81
+ export declare function codexRelayAuth(headers: IncomingHttpHeaders): CodexRelayAuth | undefined;
82
+ export declare class CodexBackendRelay implements SubscriptionRelay {
83
+ #private;
84
+ readonly kind: "request";
85
+ readonly dialect: "codex";
86
+ readonly httpSubmission: SubscriptionRelay["httpSubmission"];
87
+ constructor(options: CodexRelayOptions);
88
+ /**
89
+ * The merged Codex picker catalog for a `GET /v1/models` request, plus the
90
+ * upstream ETag when the live list was used. With relayable auth the stock
91
+ * list comes from the live ChatGPT backend; otherwise (or on any upstream
92
+ * failure) from the local snapshot. Returns `undefined` when no catalog can
93
+ * be built at all (no stock entry to use as a schema template).
94
+ */
95
+ mergedCatalog(headers: IncomingHttpHeaders, search: string): Effect.Effect<{
96
+ etag?: string | undefined;
97
+ models: CodexCatalogEntry[];
98
+ } | undefined, never, any>;
99
+ /**
100
+ * Whether a `POST /v1/responses` request should be relayed: it names a model
101
+ * the gateway does not serve locally AND carries relayable ChatGPT auth.
102
+ */
103
+ shouldRelayResponses(headers: IncomingHttpHeaders, model: string | undefined, servesLocally: (model: string) => boolean): boolean;
104
+ shouldRelay(headers: IncomingHttpHeaders, model: string | undefined, servesLocally: (model: string) => boolean): boolean;
105
+ /**
106
+ * Forward a Responses request to the ChatGPT Codex backend. Public string
107
+ * input is normalized to the backend's required item-list representation;
108
+ * item-list input and every non-transport header (including the client's own
109
+ * auth) go through untouched, and the upstream response (typically SSE)
110
+ * streams back unchanged.
111
+ */
112
+ relayResponses(headers: IncomingHttpHeaders, body: unknown, signal?: AbortSignal, options?: Parameters<SubscriptionRelay["relay"]>[3]): Effect.Effect<Response, Error | import("effect/PlatformError").PlatformError, any>;
113
+ relay(headers: IncomingHttpHeaders, body: Parameters<SubscriptionRelay["relay"]>[1], signal?: AbortSignal, options?: Parameters<SubscriptionRelay["relay"]>[3]): Effect.Effect<Response, Error | import("effect/PlatformError").PlatformError, any>;
114
+ snapshot(): SubscriptionAccountSetSnapshot | undefined;
115
+ close(): Effect.Effect<void, RouteKitFailure, never>;
116
+ /** Merge relayed stock slugs into an OpenAI-shape `data` model list. */
117
+ mergeDataIds(data: Array<{
118
+ id: string;
119
+ } & Record<string, unknown>>, models: readonly CodexCatalogEntry[]): Array<{
120
+ id: string;
121
+ } & Record<string, unknown>>;
122
+ }
123
+ export {};
@@ -0,0 +1,261 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { codexModelsSearch, forwardRelayHeaders } from "@velum-labs/routekit-accounts";
3
+ import { providerDefaultBaseUrl, subscriptionInfo } from "@velum-labs/routekit-registry";
4
+ import { executeInferenceRequest, executeWebRequest, RouteKitFailure, routeKitError, toRouteKitFailure } from "@velum-labs/routekit-runtime/effect";
5
+ import { trimTrailingSlashes } from "@velum-labs/routekit-runtime/network";
6
+ import { Effect } from "effect";
7
+ import { z } from "zod";
8
+ function withCodexAccountDefaults(body) {
9
+ if (typeof body !== "object" || body === null || Array.isArray(body))
10
+ return body;
11
+ const defaults = subscriptionInfo("codex").requestDefaults;
12
+ const normalized = {
13
+ ...body,
14
+ ...(defaults?.stream !== undefined ? { stream: defaults.stream } : {}),
15
+ ...(defaults?.store !== undefined ? { store: defaults.store } : {})
16
+ };
17
+ if (defaults?.omitSampling === true) {
18
+ delete normalized.max_output_tokens;
19
+ delete normalized.temperature;
20
+ delete normalized.top_p;
21
+ }
22
+ return normalized;
23
+ }
24
+ function withCodexResponsesInput(body) {
25
+ if (typeof body !== "object" || body === null || Array.isArray(body))
26
+ return body;
27
+ const record = body;
28
+ if (typeof record.input !== "string")
29
+ return body;
30
+ return {
31
+ ...record,
32
+ input: [
33
+ {
34
+ type: "message",
35
+ role: "user",
36
+ content: [{ type: "input_text", text: record.input }]
37
+ }
38
+ ]
39
+ };
40
+ }
41
+ const defaultProviderRelayLogger = {
42
+ warn: (message) => process.stderr.write(`${message}\n`),
43
+ error: (message) => process.stderr.write(`${message}\n`)
44
+ };
45
+ /**
46
+ * The upstream `/models` wire contract, validated structurally. There is no
47
+ * official OpenAPI document or published SDK type for the private ChatGPT
48
+ * Codex backend: the authoritative schema is codex-rs's `ModelInfo`
49
+ * (`codex-rs/protocol/src/openai_models.rs`, serde + ts-rs bindings that are
50
+ * not published to npm — `@openai/codex-sdk` only ships the Threads API), and
51
+ * it deliberately drifts across Codex releases. So validation pins exactly
52
+ * what the relay relies on — the `{ models: [...] }` envelope and each
53
+ * entry's identity (`slug`) — while every other field passes through verbatim
54
+ * (`looseObject`), which is what keeps newer/unknown fields alive on the way
55
+ * to the Codex client that DOES validate the full schema for its own version.
56
+ */
57
+ const upstreamModelsEnvelope = z.object({ models: z.array(z.unknown()) });
58
+ const stockEntrySchema = z.looseObject({ slug: z.string().min(1) });
59
+ /**
60
+ * Extract relayable ChatGPT auth from a request. Codex's ChatGPT-login auth
61
+ * provider always sends the bearer token together with a `chatgpt-account-id`
62
+ * header; the pair is what distinguishes it from a plain API key or a gateway
63
+ * bearer token, so only that combination is ever relayed.
64
+ */
65
+ export function codexRelayAuth(headers) {
66
+ const authorization = headers.authorization;
67
+ const accountId = headers["chatgpt-account-id"];
68
+ if (typeof authorization !== "string" || !authorization.startsWith("Bearer "))
69
+ return undefined;
70
+ if (typeof accountId !== "string" || accountId.length === 0)
71
+ return undefined;
72
+ return { authorization, accountId };
73
+ }
74
+ function entrySlug(entry) {
75
+ return typeof entry.slug === "string" && entry.slug.length > 0 ? entry.slug : undefined;
76
+ }
77
+ const DEFAULT_RELAY_TIMEOUT_MS = 4500;
78
+ export class CodexBackendRelay {
79
+ kind = "request";
80
+ dialect = "codex";
81
+ httpSubmission;
82
+ #backendUrl;
83
+ #catalog;
84
+ #fallbackStock;
85
+ #timeoutMs;
86
+ #logger;
87
+ #auth;
88
+ constructor(options) {
89
+ this.#backendUrl = trimTrailingSlashes(options.backendUrl ?? providerDefaultBaseUrl("codex") ?? "");
90
+ this.#catalog = options.catalog;
91
+ this.#fallbackStock = options.fallbackStock ?? (() => []);
92
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_RELAY_TIMEOUT_MS;
93
+ this.#logger = options.logger ?? defaultProviderRelayLogger;
94
+ this.#auth = options.auth ?? { kind: "client" };
95
+ if (this.#auth.kind === "accounts" && this.#auth.accounts.mode !== "codex") {
96
+ throw new Error("Codex relay requires a codex subscription account set");
97
+ }
98
+ this.httpSubmission =
99
+ this.#auth.kind === "accounts" &&
100
+ this.#backendUrl === trimTrailingSlashes(providerDefaultBaseUrl("codex") ?? "")
101
+ ? "credential-owner-http-v1"
102
+ : undefined;
103
+ }
104
+ /**
105
+ * The merged Codex picker catalog for a `GET /v1/models` request, plus the
106
+ * upstream ETag when the live list was used. With relayable auth the stock
107
+ * list comes from the live ChatGPT backend; otherwise (or on any upstream
108
+ * failure) from the local snapshot. Returns `undefined` when no catalog can
109
+ * be built at all (no stock entry to use as a schema template).
110
+ */
111
+ mergedCatalog(headers, search) {
112
+ const self = this;
113
+ return Effect.gen(function* () {
114
+ const auth = codexRelayAuth(headers);
115
+ if (auth !== undefined || self.#auth.kind === "accounts") {
116
+ const live = yield* self.#fetchUpstreamModels(headers, search).pipe(Effect.map((upstream) => {
117
+ const template = upstream.models[0];
118
+ if (template === undefined)
119
+ return undefined;
120
+ return {
121
+ models: self.#catalog(template, upstream.models),
122
+ ...(upstream.etag !== undefined ? { etag: upstream.etag } : {})
123
+ };
124
+ }), Effect.catch((error) => {
125
+ self.#logger.warn(`routekit: live Codex model catalog unavailable (${error instanceof Error ? error.message : String(error)}); using the local snapshot`);
126
+ return Effect.void;
127
+ }));
128
+ if (live !== undefined)
129
+ return live;
130
+ }
131
+ const stock = self.#fallbackStock();
132
+ const template = stock[0];
133
+ if (template === undefined)
134
+ return undefined;
135
+ return { models: self.#catalog(template, stock) };
136
+ });
137
+ }
138
+ #fetchUpstreamModels(headers, search) {
139
+ const self = this;
140
+ return Effect.gen(function* () {
141
+ const request = (injected) => {
142
+ const forwarded = forwardRelayHeaders(headers);
143
+ if (injected !== undefined) {
144
+ delete forwarded.authorization;
145
+ delete forwarded.Authorization;
146
+ delete forwarded["chatgpt-account-id"];
147
+ Object.assign(forwarded, injected);
148
+ }
149
+ return executeWebRequest(`${self.#backendUrl}/models${codexModelsSearch(search)}`, {
150
+ method: "GET",
151
+ headers: forwarded,
152
+ signal: AbortSignal.timeout(self.#timeoutMs)
153
+ }).pipe(Effect.mapError((error) => routeKitError(error)));
154
+ };
155
+ const response = yield* self.#auth.kind === "client"
156
+ ? request()
157
+ : self.#auth.accounts.execute(undefined, (credential) => request(self.#auth.kind === "accounts"
158
+ ? self.#auth.accounts.provider.authHeaders(credential)
159
+ : {}));
160
+ if (!response.ok) {
161
+ return yield* new RouteKitFailure({
162
+ message: `upstream /models returned ${response.status}`
163
+ });
164
+ }
165
+ const payload = yield* Effect.tryPromise({
166
+ try: () => response.json(),
167
+ catch: (cause) => toRouteKitFailure(cause)
168
+ });
169
+ const envelope = upstreamModelsEnvelope.safeParse(payload);
170
+ if (!envelope.success) {
171
+ return yield* new RouteKitFailure({
172
+ message: "upstream /models returned an unexpected shape (no models array)"
173
+ });
174
+ }
175
+ const models = envelope.data.models.flatMap((entry) => {
176
+ const parsed = stockEntrySchema.safeParse(entry);
177
+ return parsed.success ? [parsed.data] : [];
178
+ });
179
+ const etag = response.headers.get("etag");
180
+ return { models, ...(etag !== null ? { etag } : {}) };
181
+ });
182
+ }
183
+ /**
184
+ * Whether a `POST /v1/responses` request should be relayed: it names a model
185
+ * the gateway does not serve locally AND carries relayable ChatGPT auth.
186
+ */
187
+ shouldRelayResponses(headers, model, servesLocally) {
188
+ if (model === undefined || model.length === 0)
189
+ return false;
190
+ if (servesLocally(model))
191
+ return false;
192
+ return this.#auth.kind === "accounts" || codexRelayAuth(headers) !== undefined;
193
+ }
194
+ shouldRelay(headers, model, servesLocally) {
195
+ return this.shouldRelayResponses(headers, model, servesLocally);
196
+ }
197
+ /**
198
+ * Forward a Responses request to the ChatGPT Codex backend. Public string
199
+ * input is normalized to the backend's required item-list representation;
200
+ * item-list input and every non-transport header (including the client's own
201
+ * auth) go through untouched, and the upstream response (typically SSE)
202
+ * streams back unchanged.
203
+ */
204
+ relayResponses(headers, body, signal, options) {
205
+ const normalizedBody = withCodexResponsesInput(body);
206
+ const upstreamBody = this.#auth.kind === "accounts" ? withCodexAccountDefaults(normalizedBody) : normalizedBody;
207
+ const request = (injected) => {
208
+ const forwarded = forwardRelayHeaders(headers);
209
+ if (injected !== undefined) {
210
+ delete forwarded.authorization;
211
+ delete forwarded.Authorization;
212
+ delete forwarded["chatgpt-account-id"];
213
+ Object.assign(forwarded, injected);
214
+ }
215
+ forwarded["content-type"] = "application/json";
216
+ return executeInferenceRequest(`${this.#backendUrl}/responses`, {
217
+ method: "POST",
218
+ headers: forwarded,
219
+ body: JSON.stringify(upstreamBody),
220
+ ...(signal !== undefined ? { signal } : {})
221
+ }).pipe(Effect.mapError((error) => routeKitError(error)));
222
+ };
223
+ if (this.#auth.kind === "client")
224
+ return request();
225
+ const model = typeof body === "object" && body !== null && "model" in body && typeof body.model === "string"
226
+ ? body.model
227
+ : undefined;
228
+ const operationId = randomUUID();
229
+ return this.#auth.accounts.execute(model, (credential) => request(this.#auth.kind === "accounts" ? this.#auth.accounts.provider.authHeaders(credential) : {}), signal, {
230
+ responseMode: options?.responseMode,
231
+ onAttempt: (account) => options?.onAttribution?.({
232
+ accountAttempt: { operationId, seat: account.seat }
233
+ }),
234
+ onRetriedAttempt: (attempt) => options?.onAttribution?.({
235
+ accountAttemptResult: { operationId, ...attempt }
236
+ })
237
+ });
238
+ }
239
+ relay(headers, body, signal, options) {
240
+ return this.relayResponses(headers, body, signal, options);
241
+ }
242
+ snapshot() {
243
+ return this.#auth.kind === "accounts" ? this.#auth.accounts.statusSnapshot() : undefined;
244
+ }
245
+ close() {
246
+ return this.#auth.kind === "accounts" ? this.#auth.accounts.close() : Effect.void;
247
+ }
248
+ /** Merge relayed stock slugs into an OpenAI-shape `data` model list. */
249
+ mergeDataIds(data, models) {
250
+ const seen = new Set(data.map((entry) => entry.id));
251
+ const merged = [...data];
252
+ for (const entry of models) {
253
+ const slug = entrySlug(entry);
254
+ if (slug === undefined || seen.has(slug))
255
+ continue;
256
+ seen.add(slug);
257
+ merged.push({ id: slug, object: "model", owned_by: "codex-relay" });
258
+ }
259
+ return merged;
260
+ }
261
+ }
@@ -0,0 +1,2 @@
1
+ import type { SubscriptionProvider } from "@velum-labs/routekit-accounts";
2
+ export declare function codexProvider(): SubscriptionProvider<"codex">;