@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -0,0 +1,69 @@
1
+ // Shared client export constants.
2
+ import type { OcxConfig } from "../../types";
3
+
4
+
5
+ /** Provider key owned by this project; the only key any exporter ever emits. */
6
+ export const OPENCODE_PROVIDER_ID = "opencodex";
7
+
8
+ export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
9
+
10
+ /**
11
+ * Env var carrying the proxy admission key to opencode. The config only ever holds the
12
+ * `{env:...}` reference, so the secret never lands on disk. opencode substitutes it at
13
+ * load time.
14
+ */
15
+ export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY";
16
+
17
+ /** Env reference shared by apiKey and the dedicated proxy admission header. */
18
+ export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`;
19
+
20
+ /**
21
+ * Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays
22
+ * in the environment exactly as it does for OpenCode.
23
+ */
24
+ export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY";
25
+ export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`;
26
+
27
+ /** OpenClaw interpolates `${UPPERCASE_VAR}` and fails closed when it is unset. */
28
+ export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY";
29
+ export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
30
+
31
+ /**
32
+ * Placeholder credential for loopback-only clients (Kimi, Pi). A loopback
33
+ * bind needs no real admission key, so we emit the same placeholder the Grok
34
+ * managed block uses rather than a user secret. Pi resolves `apiKey` before
35
+ * building its model list and hides the provider when an env reference is unset.
36
+ */
37
+ export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
38
+
39
+ /**
40
+ * Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey`
41
+ * falls back to treating the literal text as the token when the variable is
42
+ * unset, which would silently ship a bogus credential — so we never emit it.
43
+ */
44
+ export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
45
+
46
+ /** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */
47
+ export const PI_API_DIALECT = "openai-completions";
48
+
49
+ /**
50
+ * opencode's config schema rejects a `limit` block that carries `context` without
51
+ * `output`, but CatalogModel has no authoritative per-model output field. Dropping
52
+ * `limit` entirely would also throw away the authoritative context window we DO have,
53
+ * so the block is emitted with this budget standing in for the missing half.
54
+ *
55
+ * The value matches REASONING_MAX_TOKENS_CEILING in src/adapters/anthropic.ts — the
56
+ * project's existing "safe ceiling across current models" figure. It is a ceiling for
57
+ * schema validity, NOT a claim about any specific model's true maximum, and it is
58
+ * clamped to the context window so a small-context model can never be emitted with
59
+ * output > context. Pi's `maxTokens` uses the same stand-in and the same clamp.
60
+ */
61
+ export const SCHEMA_REQUIRED_OUTPUT_BUDGET = 32_000;
62
+
63
+ /** Deterministic loopback default for exported provider-block helpers in tests. */
64
+ export const OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG: OcxConfig = {
65
+ port: 10100,
66
+ hostname: "127.0.0.1",
67
+ defaultProvider: "mock",
68
+ providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1" } },
69
+ } as OcxConfig;
@@ -0,0 +1,154 @@
1
+ // Shared client export contracts.
2
+ import type { OcxConfig } from "../../types";
3
+ import type { ConfigFormat } from "../../integrations/serialize";
4
+
5
+ /**
6
+ * One entry opencodex owns inside a client's config: the JSON path to it and
7
+ * the value we put there.
8
+ *
9
+ * A path list rather than a single provider key because ownership is not
10
+ * always one entry — Kimi owns its provider block AND one model entry per
11
+ * model, and a writer that only knew about the provider would strand the rest
12
+ * (devlog 260802 006 §2).
13
+ */
14
+ export interface ManagedFragment {
15
+ path: readonly string[];
16
+ value: unknown;
17
+ }
18
+
19
+ /** Everything opencodex contributes to one client's config, as one unit. */
20
+ export interface ManagedContribution {
21
+ clientId: ExportClientId;
22
+ fragments: readonly ManagedFragment[];
23
+ }
24
+
25
+ export type BuildContribution = (ctx: ExportContext) => ManagedContribution;
26
+
27
+ export interface OpencodeLaunchEnv {
28
+ [key: string]: string | undefined;
29
+ }
30
+
31
+ /** Visible catalog entry keyed by the proxy's canonical namespaced selector. */
32
+ export interface OpencodeCatalogModel {
33
+ namespaced: string;
34
+ /** Hub-resolved Fast availability. Missing metadata means unavailable. */
35
+ fastRowAvailable?: boolean;
36
+ native?: boolean;
37
+ provider?: string;
38
+ id?: string;
39
+ contextWindow?: number;
40
+ displayName?: string;
41
+ /** Declared effort ladder. Exported as opencode model variants where the client reads them. */
42
+ reasoningEfforts?: readonly string[];
43
+ /**
44
+ * Declared default effort. Carried so every client export reads one deduped, visibility-
45
+ * filtered ladder per model. The opencode serializer deliberately does NOT turn it into a
46
+ * model-level setting — see {@link opencodeEffortVariants} for why.
47
+ */
48
+ defaultReasoningEffort?: string;
49
+ }
50
+
51
+ /**
52
+ * One proxy-routed model destined for a client config. Deliberately narrower than
53
+ * `CatalogModel` so a serializer cannot reach for a field that does not survive the
54
+ * `/api/models` boundary.
55
+ */
56
+ export interface ExportModel {
57
+ /** Canonical proxy selector: `provider/id`, or bare slug for native. */
58
+ namespaced: string;
59
+ /** Hub-resolved Fast availability; exporters never infer it from local config. */
60
+ fastRowAvailable?: boolean;
61
+ provider: string;
62
+ id: string;
63
+ /** Native OpenAI entry. Read by the shared label rule. */
64
+ native?: boolean;
65
+ displayName?: string;
66
+ contextWindow?: number;
67
+ inputModalities?: string[];
68
+ /** Optional effort ladder exported only to clients that support it. */
69
+ reasoningEfforts?: string[];
70
+ defaultReasoningEffort?: string;
71
+ }
72
+
73
+ export interface ExportContext {
74
+ /** `http://host:port/v1` — the OpenAI-compatible surface the client dials. */
75
+ baseUrl: string;
76
+ models: readonly ExportModel[];
77
+ /**
78
+ * Live proxy config. Only the OpenCode path reads it: a non-loopback bind moves
79
+ * admission from `apiKey` to the `x-opencodex-api-key` header.
80
+ */
81
+ config?: OcxConfig;
82
+ }
83
+
84
+ export type ExportClientId =
85
+ | "opencode"
86
+ | "pi"
87
+ | "omp"
88
+ | "hermes"
89
+ | "openclaw"
90
+ | "kimi"
91
+ | "gajae"
92
+ | "dsh"
93
+ | "mcode"
94
+ | "zcode"
95
+ | "prime"
96
+ | "aside";
97
+
98
+ export interface ExportClientSpec {
99
+ id: ExportClientId;
100
+ /** Download filename; matches the destination file's own name (003 §5). */
101
+ filename: string;
102
+ /** Canonical destination for humans. Never written to. */
103
+ destination: (env: NodeJS.ProcessEnv) => string;
104
+ /** Env var the config references; the value is never serialized. */
105
+ apiKeyEnv: string;
106
+ /** Shell line the user runs before launching the client. */
107
+ exportHint: string;
108
+ build: (ctx: ExportContext) => unknown;
109
+ /**
110
+ * Text format of the client's config file. `filename` already carries the
111
+ * extension; this drives serialization and the download media type so no
112
+ * consumer has to infer either from the name.
113
+ */
114
+ format: ConfigFormat;
115
+ /**
116
+ * Count models in THIS client's document shape. Required so a new client
117
+ * cannot be added without teaching the summarizer about it — the old
118
+ * "anything that is not OpenCode must be Pi" branch was a latent bug.
119
+ */
120
+ summarize: (document: unknown) => { modelCount: number; modelsWithoutLimits: number };
121
+ /**
122
+ * The fragments opencodex owns inside this client's config. Only the builder
123
+ * knows where a client keeps our entries, so ownership paths originate here
124
+ * rather than being re-derived by the writer.
125
+ */
126
+ buildContribution: BuildContribution;
127
+ /**
128
+ * True when the generated integration deliberately supports loopback only.
129
+ *
130
+ * `/v1/chat/completions` rejects bearer credentials and requires the
131
+ * dedicated `x-opencodex-api-key` header (AUTH_MATRIX in
132
+ * src/server/auth-cors.ts). If this exporter cannot safely emit that header,
133
+ * it refuses a remote bind rather than generating a config that 401s. Same
134
+ * reasoning as the Grok managed block's non-loopback refusal.
135
+ */
136
+ loopbackOnly: boolean;
137
+ }
138
+
139
+ export interface PiModelEntry {
140
+ id: string;
141
+ name: string;
142
+ input: string[];
143
+ contextWindow?: number;
144
+ maxTokens?: number;
145
+ /** Advertised when the catalog row carries a non-empty effort ladder. */
146
+ reasoning?: true;
147
+ /**
148
+ * Constrains pi's own level scale (minimal..max) to the declared ladder: members map to
149
+ * themselves, everything else is hidden (`null`). Without it pi would offer levels the
150
+ * ladder does not contain — harmless for provider-config ladders (the proxy clamps those
151
+ * at the wire) but a real 400 risk for custom-row ladders, which are advertisement-only.
152
+ */
153
+ thinkingLevelMap?: Record<string, string | null>;
154
+ }
@@ -0,0 +1,132 @@
1
+ // DSH config export.
2
+ import type { ExportModel, ExportContext, ManagedContribution } from "./contracts";
3
+ import type { OcxConfig } from "../../types";
4
+ import { providerCodexAccountMode } from "../../providers/registry";
5
+ import { normalizeExportModels, authoritativeContextWindow, exportModelLabel, singleFragment } from "./model-metadata";
6
+ import { OPENCODE_PROVIDER_ID } from "./constants";
7
+
8
+
9
+ /** DSH rc.6 accepts text/image; unknown values degrade to text, while audio-only cannot be represented. */
10
+ function dshInputModalities(modalities: readonly string[] | undefined): string[] | null {
11
+ const declared = modalities ?? [];
12
+ if (declared.length === 0) return ["text"];
13
+ const kept: string[] = [];
14
+ for (const value of declared) {
15
+ if ((value === "text" || value === "image") && !kept.includes(value)) kept.push(value);
16
+ }
17
+ if (kept.length > 0) return kept;
18
+ return declared.every(value => value === "audio") ? null : ["text"];
19
+ }
20
+
21
+ export type DshReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
22
+ export type DshWireReasoningEffort = DshReasoningEffort | "ultra";
23
+
24
+ export interface DshModelEntry {
25
+ id: string;
26
+ name: string;
27
+ input: string[];
28
+ contextWindow?: number;
29
+ reasoningEfforts?: Partial<Record<DshReasoningEffort, DshWireReasoningEffort>>;
30
+ }
31
+
32
+ export interface DshProviderBlock {
33
+ displayName: "OpenCodex";
34
+ api: "openai-responses";
35
+ baseURL: string;
36
+ headers: { Authorization: "Bearer ocx_data_dsh" };
37
+ models: DshModelEntry[];
38
+ }
39
+
40
+ export interface DshGeneratedConfig {
41
+ "llm-pi-ai": {
42
+ providers: Record<string, DshProviderBlock>;
43
+ };
44
+ }
45
+
46
+ const DSH_EFFORT_ORDER: readonly DshReasoningEffort[] = ["low", "medium", "high", "xhigh", "max"];
47
+
48
+ function dshReasoningEfforts(model: ExportModel): DshModelEntry["reasoningEfforts"] {
49
+ const offered = new Set<string>();
50
+ for (const raw of model.reasoningEfforts ?? []) {
51
+ const effort = raw.trim().toLowerCase();
52
+ if (effort === "ultra" || DSH_EFFORT_ORDER.includes(effort as DshReasoningEffort)) offered.add(effort);
53
+ }
54
+ if (offered.size === 0) return undefined;
55
+ const entries: Array<[DshReasoningEffort, DshWireReasoningEffort]> = [];
56
+ for (const effort of DSH_EFFORT_ORDER) {
57
+ if (effort !== "max") {
58
+ if (offered.has(effort)) entries.push([effort, effort]);
59
+ continue;
60
+ }
61
+ // DSH's key is the selectable level; the value is what it sends on the
62
+ // wire. Preserve OpenCodex's `ultra` spelling when that is the only
63
+ // highest effort, exactly like the rc.6 `max: ultra` contract.
64
+ if (offered.has("max")) entries.push(["max", "max"]);
65
+ else if (offered.has("ultra")) entries.push(["max", "ultra"]);
66
+ }
67
+ return Object.fromEntries(entries);
68
+ }
69
+
70
+ function isKnownSafeDshCombo(model: ExportModel, config: OcxConfig): boolean {
71
+ const combos = (config as { combos?: unknown }).combos;
72
+ if (typeof combos !== "object" || combos === null || Array.isArray(combos)) return false;
73
+ const combo = (combos as Record<string, unknown>)[model.id];
74
+ if (typeof combo !== "object" || combo === null || Array.isArray(combo)) return false;
75
+ const targets = (combo as { targets?: unknown }).targets;
76
+ if (!Array.isArray(targets) || targets.length === 0) return false;
77
+ return targets.every(target => {
78
+ if (typeof target !== "object" || target === null || Array.isArray(target)) return false;
79
+ const provider = (target as { provider?: unknown }).provider;
80
+ const modelId = (target as { model?: unknown }).model;
81
+ return typeof provider === "string"
82
+ && provider.length > 0
83
+ && provider === provider.trim()
84
+ && provider !== "openai"
85
+ && typeof modelId === "string"
86
+ && modelId.length > 0
87
+ && modelId === modelId.trim();
88
+ });
89
+ }
90
+
91
+ export function buildDshClientConfig(ctx: ExportContext): DshGeneratedConfig {
92
+ const direct = providerCodexAccountMode("openai", ctx.config?.providers?.openai) === "direct";
93
+ const models: DshModelEntry[] = [];
94
+ for (const model of normalizeExportModels(ctx.models)) {
95
+ if (direct && (model.native === true || model.provider === "openai")) continue;
96
+ if (direct && model.provider === "combo" && (!ctx.config || !isKnownSafeDshCombo(model, ctx.config))) continue;
97
+ const input = dshInputModalities(model.inputModalities);
98
+ if (input === null) continue;
99
+ const contextWindow = authoritativeContextWindow(model.contextWindow);
100
+ const reasoningEfforts = dshReasoningEfforts(model);
101
+ models.push({
102
+ id: model.namespaced,
103
+ name: exportModelLabel(model),
104
+ input,
105
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
106
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
107
+ });
108
+ }
109
+ return {
110
+ "llm-pi-ai": {
111
+ providers: {
112
+ [OPENCODE_PROVIDER_ID]: {
113
+ displayName: "OpenCodex",
114
+ api: "openai-responses",
115
+ baseURL: ctx.baseUrl,
116
+ headers: { Authorization: "Bearer ocx_data_dsh" },
117
+ models,
118
+ },
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ export function summarizeDsh(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
125
+ const models = (document as DshGeneratedConfig | undefined)?.["llm-pi-ai"]?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
126
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
127
+ }
128
+
129
+ export function buildDshContribution(ctx: ExportContext): ManagedContribution {
130
+ const doc = buildDshClientConfig(ctx);
131
+ return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
132
+ }
@@ -0,0 +1,29 @@
1
+ import type { OpencodeCatalogModel } from "./contracts";
2
+
3
+ const FAST_ROW_SUFFIX = "--fast";
4
+
5
+ /**
6
+ * Expand only hub-resolved availability; older hubs without the field advertise no Fast.
7
+ * Reserve every exact input selector before synthesis and retain the first duplicate.
8
+ * Keep routing/capability metadata intact: only the selector, label and availability change.
9
+ */
10
+ export function expandFastExportModels<T extends OpencodeCatalogModel>(models: readonly T[]): T[] {
11
+ const exact = new Map<string, T>();
12
+ for (const model of models) {
13
+ if (!exact.has(model.namespaced)) exact.set(model.namespaced, model);
14
+ }
15
+ const expanded = [...exact.values()];
16
+ for (const model of exact.values()) {
17
+ if (model.fastRowAvailable !== true) continue;
18
+ const namespaced = `${model.namespaced}${FAST_ROW_SUFFIX}`;
19
+ if (exact.has(namespaced)) continue;
20
+ expanded.push({
21
+ ...model,
22
+ namespaced,
23
+ displayName: `${model.displayName || model.id || model.namespaced} Fast`,
24
+ // Re-normalizing cannot synthesize a Fast row from this synthetic row.
25
+ fastRowAvailable: false,
26
+ });
27
+ }
28
+ return expanded;
29
+ }
@@ -0,0 +1,83 @@
1
+ // MiniMax Code config export.
2
+ import type { ExportContext, ManagedContribution } from "./contracts";
3
+ import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata";
4
+ import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
5
+ import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
6
+
7
+
8
+ export interface McodeProviderBlock {
9
+ name: "OpenCodex";
10
+ kind: "custom";
11
+ enabled: true;
12
+ api: "anthropic-messages";
13
+ options: {
14
+ apiKey: string;
15
+ baseURL: string;
16
+ authMode: "api-key";
17
+ };
18
+ models: Record<string, McodeModelEntry>;
19
+ }
20
+
21
+ export interface McodeModelEntry {
22
+ /** MCode uses this value for context accounting and compaction. */
23
+ limit?: { context: number };
24
+ /** MCode exposes these exact levels in `/model` and sends the selected effort. */
25
+ thinking?: { effortOptions: string[] };
26
+ }
27
+
28
+ export interface McodeGeneratedConfig {
29
+ custom_provider: Record<string, McodeProviderBlock>;
30
+ }
31
+
32
+ /**
33
+ * MiniMax Code's `provider add` command persists custom providers under
34
+ * `custom_provider.<id>`. Its current model schema reads `limit.context` for
35
+ * context accounting and `thinking.effortOptions` for the `/model` effort
36
+ * control. Do not emit the removed `thinking.effort` / `defaultEffort` fields:
37
+ * MCode 0.1.6 migrates those into options and keeps the selected effort in the
38
+ * session. Do not emit `defaultModel` either: connecting a client must not
39
+ * silently replace the user's current model selection.
40
+ */
41
+ export function buildMcodeClientConfig(ctx: ExportContext): McodeGeneratedConfig {
42
+ const models: Record<string, McodeModelEntry> = {};
43
+ for (const model of normalizeExportModels(ctx.models)) {
44
+ const entry: McodeModelEntry = {};
45
+ const context = authoritativeContextWindow(model.contextWindow);
46
+ if (context !== undefined) entry.limit = { context };
47
+ // `none` is an internal Codex catalog sentinel, not an MCode effort. MCode
48
+ // forwards every option as `output_config.effort` while keeping adaptive
49
+ // thinking enabled, and the Anthropic ingress deliberately accepts only
50
+ // minimal..ultra. Advertising `none` would therefore create a selectable
51
+ // value that cannot disable reasoning and is not forwarded as an effort.
52
+ const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
53
+ ?.filter(effort => effort !== "none");
54
+ if (efforts && efforts.length > 0) entry.thinking = { effortOptions: efforts };
55
+ models[model.namespaced] = entry;
56
+ }
57
+ return {
58
+ custom_provider: {
59
+ [OPENCODE_PROVIDER_ID]: {
60
+ name: "OpenCodex",
61
+ kind: "custom",
62
+ enabled: true,
63
+ api: "anthropic-messages",
64
+ options: {
65
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
66
+ baseURL: ctx.baseUrl.replace(/\/v1\/?$/, ""),
67
+ authMode: "api-key",
68
+ },
69
+ models,
70
+ },
71
+ },
72
+ };
73
+ }
74
+
75
+ export function summarizeMcode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
76
+ const models = Object.values((document as McodeGeneratedConfig | undefined)?.custom_provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
77
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
78
+ }
79
+
80
+ export function buildMcodeContribution(ctx: ExportContext): ManagedContribution {
81
+ const doc = buildMcodeClientConfig(ctx);
82
+ return singleFragment("mcode", ["custom_provider", OPENCODE_PROVIDER_ID], doc.custom_provider[OPENCODE_PROVIDER_ID]);
83
+ }
@@ -0,0 +1,108 @@
1
+ // Shared client export model metadata.
2
+ import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants";
3
+ import { expandFastExportModels } from "./fast-models";
4
+ import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts";
5
+ import type { OcxConfig } from "../../types";
6
+ import { shouldInjectApiAuthHeader } from "../../codex/inject";
7
+
8
+
9
+ /**
10
+ * Authoritative context window, or undefined. Never guesses: a missing, non-finite, or
11
+ * non-positive value means the serializer omits every context-derived field.
12
+ */
13
+ export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
14
+ if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
15
+ const integer = Math.floor(contextWindow);
16
+ return integer > 0 ? integer : undefined;
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ /** Schema-required output budget for a known context window. */
22
+ export function outputBudgetFor(context: number): number {
23
+ return Math.min(SCHEMA_REQUIRED_OUTPUT_BUDGET, context);
24
+ }
25
+
26
+ /**
27
+ * Modalities a given client's schema will actually accept.
28
+ *
29
+ * Our internal vocabulary is `text | image | audio` (ALLOWED_INPUT_MODALITIES in
30
+ * src/server/management/model-routes.ts). Pi and Gajae accept only
31
+ * `text | image`, and both reject the WHOLE config file over one out-of-enum
32
+ * value — Gajae reports `/providers/opencodex/models/N/input/2: Invalid option`
33
+ * and falls back to its built-in list, Pi returns an empty model config. So a
34
+ * single `audio` model takes every routed model down with it. That is not
35
+ * hypothetical: zenmux/meta-muse-spark-1.1 advertises audio and did exactly
36
+ * this. It is also the same defect the Codex catalog had with `video`, where
37
+ * the app showed zero apps (tests/codex-integration/catalog-input-modality-enum.test.ts).
38
+ *
39
+ * UNKNOWN and INCOMPATIBLE are different inputs, and the Codex fix could
40
+ * conflate them safely only because its enum is wider. A model with nothing
41
+ * declared is unknown, and `text` is the honest floor — every routed model takes
42
+ * prompts. A model declaring `["audio"]` and nothing else is incompatible with a
43
+ * text|image client, and rewriting it to `["text"]` would advertise a capability
44
+ * it does not have. That input is reachable three ways: `ocx models add
45
+ * --modalities audio`, `/api/custom-models`, and provider discovery.
46
+ *
47
+ * So unknown falls back to text and incompatible returns null, which drops the
48
+ * row. Omitting a model costs the user a line in a picker; fabricating `text`
49
+ * costs them a model that fails at call time with no explanation.
50
+ *
51
+ * Deliberately NOT applied in `ExportModel` construction: the management and CLI
52
+ * boundaries carry catalog modalities verbatim on purpose, and stripping `audio`
53
+ * globally would destroy valid metadata before the destination is known.
54
+ */
55
+ const CLIENT_INPUT_MODALITIES: Record<"pi" | "gajae", ReadonlySet<string>> = {
56
+ pi: new Set(["text", "image"]),
57
+ gajae: new Set(["text", "image"]),
58
+ };
59
+
60
+ /** `null` means the model cannot be represented for this client — drop the row. */
61
+ export function inputModalitiesForClient(
62
+ client: "pi" | "gajae",
63
+ modalities: readonly string[] | undefined,
64
+ ): string[] | null {
65
+ const declared = modalities ?? [];
66
+ if (declared.length === 0) return ["text"];
67
+ const accepted = CLIENT_INPUT_MODALITIES[client];
68
+ const kept: string[] = [];
69
+ for (const value of declared) {
70
+ if (accepted.has(value) && !kept.includes(value)) kept.push(value);
71
+ }
72
+ return kept.length > 0 ? kept : null;
73
+ }
74
+
75
+ /**
76
+ * Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
77
+ * provider suffix is what makes two same-named models from different upstreams
78
+ * distinguishable in a client's model picker.
79
+ */
80
+ export function exportModelLabel(model: OpencodeCatalogModel): string {
81
+ const providerLabel = model.native ? "native" : (model.provider ?? "routed");
82
+ const id = model.id ?? model.namespaced;
83
+ if (model.displayName && model.displayName.length > 0) {
84
+ return `${model.displayName} (${providerLabel})`;
85
+ }
86
+ return `${id} (${providerLabel})`;
87
+ }
88
+
89
+ /**
90
+ * Shared precondition for every serializer: expand hub-approved Fast rows, drop duplicate
91
+ * `namespaced` (first wins, native rows lead `/api/models`) and sort so calls with the same
92
+ * models produce identical bytes. Stability matters because the GUI shows a diffable
93
+ * preview and agents may checksum the payload.
94
+ */
95
+ export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] {
96
+ return expandFastExportModels(models)
97
+ .sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0));
98
+ }
99
+
100
+ /** Extra headers a non-loopback bind needs, or nothing on loopback. */
101
+ export function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record<string, string> | undefined {
102
+ return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined;
103
+ }
104
+
105
+ /** One fragment at `path`, built from this client's own document. */
106
+ export function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution {
107
+ return { clientId, fragments: [{ path, value }] };
108
+ }
@@ -0,0 +1,104 @@
1
+ // Oh My Pi config export.
2
+ import type { PiModelEntry, ExportModel, ExportContext, ManagedContribution } from "./contracts";
3
+ import { PI_API_DIALECT, OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
4
+ import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, outputBudgetFor, singleFragment } from "./model-metadata";
5
+
6
+
7
+ /**
8
+ * omp accepts a model-level API override. Keep the provider on Chat
9
+ * Completions so routed providers retain their established wire format, while
10
+ * native OpenAI models can use the lossless Responses surface.
11
+ */
12
+ export interface OmpModelEntry extends PiModelEntry {
13
+ api?: "openai-responses";
14
+ /** omp requires this flag before it honors a thinking block. */
15
+ reasoning?: true;
16
+ thinking?: {
17
+ mode: "effort";
18
+ efforts: string[];
19
+ defaultLevel?: string;
20
+ };
21
+ }
22
+
23
+ export interface OmpProviderBlock {
24
+ baseUrl: string;
25
+ api: typeof PI_API_DIALECT;
26
+ apiKey: string;
27
+ models: OmpModelEntry[];
28
+ }
29
+
30
+ export interface OmpGeneratedConfig {
31
+ providers: Record<string, OmpProviderBlock>;
32
+ }
33
+
34
+ /**
35
+ * omp validates model entries strictly. These are its documented effort
36
+ * values; omit an unknown value rather than invalidating the whole provider.
37
+ */
38
+ const OMP_EFFORT_VOCABULARY = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
39
+
40
+ function ompEfforts(model: ExportModel): string[] {
41
+ const efforts: string[] = [];
42
+ for (const effort of model.reasoningEfforts ?? []) {
43
+ const normalized = effort.trim().toLowerCase();
44
+ if (OMP_EFFORT_VOCABULARY.has(normalized) && !efforts.includes(normalized)) {
45
+ efforts.push(normalized);
46
+ }
47
+ }
48
+ return efforts;
49
+ }
50
+
51
+ /**
52
+ * omp's models.yml is Pi-like, but it supports effort metadata and a per-model
53
+ * API dialect. Native OpenAI models use Responses; all routed models inherit
54
+ * the provider's existing Chat Completions dialect.
55
+ */
56
+ export function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig {
57
+ const models: OmpModelEntry[] = [];
58
+ for (const model of normalizeExportModels(ctx.models)) {
59
+ const input = inputModalitiesForClient("pi", model.inputModalities);
60
+ if (input === null) continue;
61
+ const entry: OmpModelEntry = {
62
+ id: model.namespaced,
63
+ name: exportModelLabel(model),
64
+ input,
65
+ ...(model.native && model.provider === "openai" ? { api: "openai-responses" } : {}),
66
+ };
67
+ const context = authoritativeContextWindow(model.contextWindow);
68
+ if (context !== undefined) {
69
+ entry.contextWindow = context;
70
+ entry.maxTokens = outputBudgetFor(context);
71
+ }
72
+ const efforts = ompEfforts(model);
73
+ if (efforts.length > 0) {
74
+ const defaultLevel = model.defaultReasoningEffort?.trim().toLowerCase();
75
+ entry.reasoning = true;
76
+ entry.thinking = {
77
+ mode: "effort",
78
+ efforts,
79
+ ...(defaultLevel && efforts.includes(defaultLevel) ? { defaultLevel } : {}),
80
+ };
81
+ }
82
+ models.push(entry);
83
+ }
84
+ return {
85
+ providers: {
86
+ [OPENCODE_PROVIDER_ID]: {
87
+ baseUrl: ctx.baseUrl,
88
+ api: PI_API_DIALECT,
89
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
90
+ models,
91
+ },
92
+ },
93
+ };
94
+ }
95
+
96
+ export function summarizeOmp(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
97
+ const models = (document as OmpGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
98
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
99
+ }
100
+
101
+ export function buildOmpContribution(ctx: ExportContext): ManagedContribution {
102
+ const doc = buildOmpClientConfig(ctx);
103
+ return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
104
+ }