@bitkyc08/opencodex 2.76.0 → 2.77.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 (189) hide show
  1. package/gui/dist/assets/App-COdEeqSj.css +1 -0
  2. package/gui/dist/assets/App-DEpCJEP_.js +54 -0
  3. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-gloIYNaW.js} +1 -1
  4. package/gui/dist/assets/index-Cd7ittMv.css +1 -0
  5. package/gui/dist/assets/index-DwJKFJm5.js +94 -0
  6. package/gui/dist/assets/quota-summary-DGcEJU3h.js +1 -0
  7. package/gui/dist/index.html +4 -3
  8. package/package.json +1 -1
  9. package/src/adapters/antigravity-refusal.ts +28 -0
  10. package/src/adapters/cursor/protobuf-request.ts +6 -0
  11. package/src/adapters/cursor/request-builder.ts +3 -0
  12. package/src/adapters/cursor/structured-output.ts +13 -0
  13. package/src/adapters/cursor/types.ts +2 -0
  14. package/src/adapters/ollama-native.ts +29 -2
  15. package/src/adapters/openai-responses/passthrough.ts +133 -103
  16. package/src/adapters/openai-responses/request-strips.ts +10 -4
  17. package/src/adapters/zed.ts +26 -4
  18. package/src/bridge/response-json.ts +14 -14
  19. package/src/bridge/sse.ts +15 -5
  20. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  21. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  22. package/src/claude/agents-inject.ts +4 -43
  23. package/src/claude/gateway-cache.ts +16 -13
  24. package/src/claude/inbound-model-options.ts +9 -2
  25. package/src/claude/intercept/cli-catalog.ts +27 -16
  26. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  27. package/src/claude/intercept/picker-budget.ts +41 -0
  28. package/src/claude/outbound.ts +2 -0
  29. package/src/claude/subagent-force-status.ts +53 -0
  30. package/src/claude/subagent-model.ts +93 -0
  31. package/src/cli/account-api.ts +3 -0
  32. package/src/cli/account.ts +3 -1
  33. package/src/cli/agent.ts +10 -0
  34. package/src/cli/capabilities.ts +9 -0
  35. package/src/cli/chatgpt-command.ts +10 -7
  36. package/src/cli/claude.ts +22 -6
  37. package/src/cli/codex-shim-readiness.ts +89 -0
  38. package/src/cli/connect.ts +19 -0
  39. package/src/cli/dispatch.ts +3 -3
  40. package/src/cli/export-command.ts +20 -9
  41. package/src/cli/gui.ts +8 -3
  42. package/src/cli/help-catalog.ts +84 -0
  43. package/src/cli/help-models-context.ts +17 -0
  44. package/src/cli/help-navigation.ts +50 -0
  45. package/src/cli/help-recovery.ts +66 -0
  46. package/src/cli/help.ts +52 -8
  47. package/src/cli/models-runtime.ts +2 -1
  48. package/src/cli/opencode.ts +54 -10
  49. package/src/cli/provider.ts +3 -29
  50. package/src/cli/registry.ts +29 -5
  51. package/src/cli/root.ts +30 -9
  52. package/src/cli/runtime-api.ts +1 -1
  53. package/src/cli/tray-proxy.ts +3 -2
  54. package/src/clients/config-export/contracts.ts +100 -0
  55. package/src/clients/config-export/droid.ts +54 -2
  56. package/src/clients/config-export/kilo.ts +12 -2
  57. package/src/clients/config-export/model-metadata.ts +10 -4
  58. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  59. package/src/clients/config-export.ts +46 -66
  60. package/src/clients/omo-role-models.ts +17 -3
  61. package/src/codex/account-credit-use.ts +4 -2
  62. package/src/codex/account-runtime-state.ts +21 -0
  63. package/src/codex/account-store.ts +11 -47
  64. package/src/codex/account-usability.ts +11 -8
  65. package/src/codex/auth-api/account-list.ts +73 -63
  66. package/src/codex/auth-api/main-account-probe.ts +25 -12
  67. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  68. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  69. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  70. package/src/codex/auth-context.ts +30 -11
  71. package/src/codex/catalog/aggregation.ts +3 -0
  72. package/src/codex/catalog/model-hints.ts +2 -1
  73. package/src/codex/catalog/model-visibility.ts +6 -3
  74. package/src/codex/catalog/parsing.ts +2 -0
  75. package/src/codex/catalog/provider-models.ts +13 -4
  76. package/src/codex/catalog/retained-sync.ts +2 -1
  77. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  78. package/src/codex/convergence.ts +2 -1
  79. package/src/codex/main-account.ts +144 -11
  80. package/src/codex/quota-refresh-outcome.ts +17 -2
  81. package/src/codex/quota-types.ts +33 -1
  82. package/src/codex/quota.ts +56 -15
  83. package/src/codex/routing/cooldown-math.ts +10 -4
  84. package/src/codex/routing/selection.ts +11 -4
  85. package/src/codex/routing.ts +9 -9
  86. package/src/codex/shim-path-resolution.ts +133 -0
  87. package/src/codex/shim.ts +39 -39
  88. package/src/codex/subagent-model-fallback.ts +2 -1
  89. package/src/combos/failover.ts +82 -1
  90. package/src/combos/jev.ts +13 -3
  91. package/src/config/diagnostics.ts +4 -0
  92. package/src/config/load-degrade.ts +4 -0
  93. package/src/config/provider-validation.ts +3 -0
  94. package/src/config/schema/compaction-triggers.ts +8 -0
  95. package/src/config/schema/config-schema.ts +11 -0
  96. package/src/config/schema/leaf-validators.ts +5 -1
  97. package/src/config/subagent-models.ts +12 -0
  98. package/src/generated/compatibility-version.json +245 -153
  99. package/src/grok/session-identity.ts +33 -0
  100. package/src/integrations/catalog-refresh.ts +1 -1
  101. package/src/integrations/mutation-plan.ts +4 -2
  102. package/src/integrations/state.ts +68 -6
  103. package/src/integrations/writer.ts +18 -4
  104. package/src/lib/gui-pair-capability.ts +17 -0
  105. package/src/lib/provider-client-headers.ts +62 -0
  106. package/src/oauth/anthropic-continuity.ts +65 -0
  107. package/src/oauth/anthropic-identity.ts +75 -0
  108. package/src/oauth/anthropic-routing.ts +3 -3
  109. package/src/oauth/anthropic.ts +7 -1
  110. package/src/oauth/generic-account-failover.ts +21 -0
  111. package/src/oauth/health.ts +32 -5
  112. package/src/oauth/index.ts +36 -27
  113. package/src/oauth/local-token-detect.ts +7 -0
  114. package/src/oauth/refresh-policy.ts +2 -0
  115. package/src/oauth/store.ts +55 -6
  116. package/src/oauth/types.ts +9 -0
  117. package/src/protocols/encoders/messages.ts +4 -3
  118. package/src/providers/antigravity-effort-families.ts +75 -0
  119. package/src/providers/antigravity-models.ts +54 -16
  120. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  121. package/src/providers/derive.ts +2 -0
  122. package/src/providers/model-rename-fields.ts +3 -0
  123. package/src/providers/new-model-policy.ts +48 -10
  124. package/src/providers/registry/entries-extended.ts +3 -0
  125. package/src/providers/registry/model-ids.ts +2 -0
  126. package/src/providers/registry/model-seeds.ts +5 -0
  127. package/src/providers/registry/types.ts +4 -0
  128. package/src/responses/compaction.ts +19 -0
  129. package/src/router.ts +6 -0
  130. package/src/server/auth-cors.ts +9 -0
  131. package/src/server/chat-completions.ts +10 -0
  132. package/src/server/claude-messages.ts +18 -11
  133. package/src/server/droid-reasoning-default.ts +50 -0
  134. package/src/server/github-copilot-responses-repair.ts +3 -6
  135. package/src/server/grok-responses-control-frame.ts +2 -2
  136. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  137. package/src/server/gui-session.ts +33 -11
  138. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  139. package/src/server/index/serve-options.ts +2 -1
  140. package/src/server/management/agent-settings-routes.ts +6 -129
  141. package/src/server/management/config-routes.ts +1 -1
  142. package/src/server/management/integration-routes.ts +64 -6
  143. package/src/server/management/link-routes.ts +20 -13
  144. package/src/server/management/model-row-export-metadata.ts +150 -0
  145. package/src/server/management/model-rows.ts +148 -9
  146. package/src/server/management/route-registry.ts +4 -3
  147. package/src/server/management/shared.ts +19 -5
  148. package/src/server/management/subagent-model-routes.ts +214 -0
  149. package/src/server/relay.ts +14 -44
  150. package/src/server/request-log-generation-window.ts +38 -0
  151. package/src/server/request-log.ts +13 -0
  152. package/src/server/responses/adapter-dispatch.ts +100 -1
  153. package/src/server/responses/codex-auth-error.ts +10 -2
  154. package/src/server/responses/combo-stream-preflight.ts +29 -3
  155. package/src/server/responses/compaction-routing.ts +10 -1
  156. package/src/server/responses/core-auth.ts +57 -26
  157. package/src/server/responses/core-codex-account.ts +6 -3
  158. package/src/server/responses/core-combo-failure.ts +5 -1
  159. package/src/server/responses/core-combo-native.ts +7 -1
  160. package/src/server/responses/core-combo.ts +18 -1
  161. package/src/server/responses/core-normalize.ts +7 -0
  162. package/src/server/responses/core-options.ts +5 -0
  163. package/src/server/responses/fetch-helpers.ts +9 -0
  164. package/src/server/responses/passthrough-delivery.ts +1 -1
  165. package/src/server/responses/policy-fallback.ts +1 -1
  166. package/src/server/responses/request-prepare.ts +5 -0
  167. package/src/server/responses/request-spend.ts +30 -30
  168. package/src/server/responses-custom-tool-repair.ts +2 -2
  169. package/src/server/responses-hosted-image-display.ts +70 -9
  170. package/src/server/responses-terminal-repair.ts +15 -2
  171. package/src/server/sse-frame-buffer.ts +38 -36
  172. package/src/server/sse-payload-rewrite.ts +78 -49
  173. package/src/server/ws-bridge.ts +1 -11
  174. package/src/service/windows-taskxml.ts +1 -2
  175. package/src/service-manager-probe.ts +36 -2
  176. package/src/types/config.ts +4 -0
  177. package/src/types/provider.ts +18 -0
  178. package/src/update/npm-invocation.mjs +35 -3
  179. package/src/update/proxy-liveness-probe.mjs +38 -8
  180. package/src/usage/expected-prices.ts +9 -0
  181. package/src/usage/log.ts +7 -0
  182. package/src/usage/summary.ts +82 -1
  183. package/src/web-search/loop.ts +11 -8
  184. package/src/web-search/run-turn-loop.ts +10 -7
  185. package/gui/dist/assets/App-CddorHdA.js +0 -51
  186. package/gui/dist/assets/App-DMudtISW.css +0 -1
  187. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  188. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  189. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
package/src/cli/root.ts CHANGED
@@ -8,11 +8,13 @@
8
8
  * the exit paths and the shim preflight, then returns the dispatchable head
9
9
  * for the command switch in src/cli/index.ts.
10
10
  */
11
- import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
11
+ import { printFullUsage, printSubcommandUsage, printUsage, printVersion } from "./help";
12
12
  import { parseReadyArgs, type ReadyArgs } from "./ready";
13
13
  import { parseResolveArgs, type ResolveArgs } from "./resolve";
14
14
  import { parseStopApproval } from "./stop-approval";
15
15
  import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
16
+ import { findCommand } from "./registry";
17
+ import { printUnknownCommand } from "./help-recovery";
16
18
 
17
19
  export interface CliHead {
18
20
  kind: "version" | "help" | "ready" | "resolve" | "command";
@@ -20,6 +22,10 @@ export interface CliHead {
20
22
  args: string[];
21
23
  /** For kind "help": the subcommand whose usage should print, if any. */
22
24
  helpTarget?: string;
25
+ /** Explicit multi-token help topic; never passed to command execution. */
26
+ helpPath?: string[];
27
+ /** Complete static reference requested from root help. */
28
+ helpAll?: boolean;
23
29
  /** Present only for `ready`; undefined when the ready args failed to parse. */
24
30
  readyArgs?: ReadyArgs;
25
31
  /** Present only for `resolve`; undefined when the resolve args failed to parse. */
@@ -32,19 +38,26 @@ export function parseCliHead(argv: string[]): CliHead {
32
38
  if (command === "--version" || command === "-v" || command === "version") {
33
39
  return { kind: "version", command, args };
34
40
  }
35
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
36
- // `ocx help <sub>` carries the subcommand; bare/flag help prints the full usage.
41
+ const boundary = argv.indexOf("--");
42
+ const prefix = boundary < 0 ? argv : argv.slice(0, boundary);
43
+ const rootHelp = command === undefined || command === "help" || command === "--help" || command === "-h";
44
+ const commandHelp = prefix[1] === "help";
45
+ if (rootHelp || commandHelp || prefix.slice(1).some(value => value === "--help" || value === "-h")) {
46
+ const topic = rootHelp ? prefix.slice(1)
47
+ : commandHelp ? [command!, ...prefix.slice(2)] : prefix;
48
+ // Options and their operands are not part of a help topic. Keep argv intact.
49
+ const option = topic.findIndex((value, index) => value.startsWith("-")
50
+ && (index > 0 || (rootHelp && ["--all", "--help", "-h"].includes(value))));
51
+ const path = option < 0 ? topic : topic.slice(0, option);
37
52
  return {
38
53
  kind: "help",
39
54
  command,
40
55
  args,
41
- ...(command === "help" && args[1] ? { helpTarget: args[1] } : {}),
56
+ ...(path.length ? { helpTarget: path[0] } : {}),
57
+ ...(path.length > 1 ? { helpPath: path } : {}),
58
+ ...(rootHelp && path.length === 0 && prefix.includes("--all") ? { helpAll: true } : {}),
42
59
  };
43
60
  }
44
- if (command !== "help" && hasHelpFlag(args.slice(1))) {
45
- // `ocx <cmd> --help|-h|help` prints that command's usage, not the full list.
46
- return { kind: "help", command, args, helpTarget: command };
47
- }
48
61
  // P1: pre-parse `ocx ready` and reject invalid arguments with exit 64 BEFORE
49
62
  // maybeAutoRestoreCodexShim (or any discovery/probe/filesystem-capable global
50
63
  // preflight) runs. `ready --help` / `help ready` already exited above, so this
@@ -72,7 +85,11 @@ export async function runCli(argv: string[]): Promise<CliHead> {
72
85
  printVersion();
73
86
  process.exit(0);
74
87
  case "help": {
75
- if (head.helpTarget) printSubcommandUsage(head.helpTarget);
88
+ if (head.helpAll) printFullUsage();
89
+ else if (head.helpTarget) printSubcommandUsage(head.helpTarget, head.helpPath, {
90
+ fallbackToParent: head.command !== "help" && head.command !== "--help"
91
+ && head.command !== "-h" && head.args[1] !== "help",
92
+ });
76
93
  else printUsage();
77
94
  process.exit(0);
78
95
  }
@@ -104,6 +121,10 @@ export async function runCli(argv: string[]): Promise<CliHead> {
104
121
  return head;
105
122
  }
106
123
  case "command":
124
+ if (head.command !== "internal" && !findCommand(head.command ?? "")) {
125
+ printUnknownCommand(head.command);
126
+ process.exit(1);
127
+ }
107
128
  if (head.command === "stop" && !parseStopApproval(head.args.slice(1)).ok) {
108
129
  console.error("Usage: ocx stop [--json [--expect-pid <pid> --expect-port <port> --expect-hostname <host> --expect-config-home <home> --expect-cli-version <version> --expect-compatibility-token <hex>]]");
109
130
  process.exit(64);
@@ -471,7 +471,7 @@ export async function readSecretBytes(
471
471
 
472
472
  export function printData(value: unknown, wantsJson: boolean, lines?: string[]): void {
473
473
  if (wantsJson || !lines) console.log(JSON.stringify(value, null, 2));
474
- else for (const line of lines) console.log(line);
474
+ else for (const line of lines) console.log(terminalSafeText(line));
475
475
  }
476
476
 
477
477
  /**
@@ -128,11 +128,12 @@ export function reobserveRestartReplacement(
128
128
  deadlineAt: number,
129
129
  observe: (end: number) => Promise<ProxyRestartDiscovery>,
130
130
  ): Promise<ProxyRestartDiscovery> {
131
- const windowMs = Math.min(5_000, deadlineAt - Date.now());
131
+ const now = Date.now();
132
+ const windowMs = Math.min(5_000, deadlineAt - now);
132
133
  if (windowMs < 1_500) return Promise.resolve({
133
134
  status: "uncertain", error: new Error("restart_reobserve_window_exhausted"),
134
135
  });
135
- const end = Date.now() + windowMs;
136
+ const end = now + windowMs;
136
137
  return pollReplacementDeparture(() => observe(end), previous, () => Date.now() + 750 < end, () => Bun.sleep(250));
137
138
  }
138
139
 
@@ -1,7 +1,10 @@
1
1
  // Shared client export contracts.
2
+ import { canonicalizeReasoningEfforts } from "../../reasoning-effort";
2
3
  import type { OcxConfig } from "../../types";
3
4
  import type { ConfigFormat } from "../../integrations/serialize";
4
5
 
6
+ export const DROID_DEFAULT_EFFORT_HEADER = "x-opencodex-droid-default-effort";
7
+
5
8
  /**
6
9
  * One entry opencodex owns inside a client's config: the JSON path to it and
7
10
  * the value we put there.
@@ -38,6 +41,12 @@ export interface OpencodeCatalogModel {
38
41
  id?: string;
39
42
  contextWindow?: number;
40
43
  maxTokens?: number;
44
+ /**
45
+ * Authoritative input ceiling, distinct from `contextWindow` (GPT-5.6 advertises 922k
46
+ * usable input under a 1.05M window). Carried from the catalog row so a client that has a
47
+ * separate input limit never treats the whole window as prompt budget.
48
+ */
49
+ maxInputTokens?: number;
41
50
  displayName?: string;
42
51
  /**
43
52
  * Declared input modalities, carried verbatim from `/api/models`. Serialized as opencode's
@@ -54,6 +63,9 @@ export interface OpencodeCatalogModel {
54
63
  * model-level setting — see {@link opencodeEffortVariants} for why.
55
64
  */
56
65
  defaultReasoningEffort?: string;
66
+ supportsTools?: boolean;
67
+ supportsReasoning?: boolean;
68
+ supportsReasoningSummaries?: boolean;
57
69
  }
58
70
 
59
71
  /**
@@ -73,10 +85,94 @@ export interface ExportModel {
73
85
  displayName?: string;
74
86
  contextWindow?: number;
75
87
  maxTokens?: number;
88
+ /** Authoritative input ceiling; optional and never guessed from the context window. */
89
+ maxInputTokens?: number;
76
90
  inputModalities?: string[];
77
91
  /** Optional effort ladder exported only to clients that support it. */
78
92
  reasoningEfforts?: string[];
79
93
  defaultReasoningEffort?: string;
94
+ supportsTools?: boolean;
95
+ supportsReasoning?: boolean;
96
+ supportsReasoningSummaries?: boolean;
97
+ }
98
+
99
+ /**
100
+ * Effective, override-applied export metadata for one model row: what a client export should
101
+ * serialize, as distinct from the raw editor representation `/api/models` also carries.
102
+ *
103
+ * Custom rows are the reason this exists. Their row fields are the operator's stored
104
+ * OVERRIDES (an absent field means "not overridden", an empty ladder means "no rungs"), so a
105
+ * serializer reading only them drops every capability the model inherits from the provider,
106
+ * the registry, or the gathered catalog. The management row list resolves that projection
107
+ * from the same canonical sources routed rows use and attaches it additively — the raw
108
+ * override fields stay exactly what the editor wrote.
109
+ */
110
+ export interface EffectiveModelExportMetadata {
111
+ contextWindow?: number;
112
+ /** Input ceiling; distinct from `contextWindow`, carried only when a source asserts one. */
113
+ maxInputTokens?: number;
114
+ /** Output ceiling (a catalog row's `maxOutputTokens`). */
115
+ maxTokens?: number;
116
+ inputModalities?: string[];
117
+ /** Effective ladder; `[]` is an explicit "no rungs" declaration and survives as `[]`. */
118
+ reasoningEfforts?: string[];
119
+ /**
120
+ * A default some canonical source declared — never one synthesized from the ladder's
121
+ * preference order, and only when it is a member of the effective ladder. An empty
122
+ * ladder suppresses it entirely: there is no rung to default to.
123
+ */
124
+ defaultReasoningEffort?: string;
125
+ supportsTools?: boolean;
126
+ supportsReasoning?: boolean;
127
+ supportsReasoningSummaries?: boolean;
128
+ }
129
+
130
+ /**
131
+ * The per-row evidence the capability booleans are derived from. Only positive, canonical
132
+ * signals — no independent provider fact tables, no name-based inference.
133
+ */
134
+ export interface ExportCapabilityEvidence {
135
+ native?: boolean;
136
+ reasoningEfforts?: readonly string[];
137
+ supportsReasoningSummaries?: boolean;
138
+ /** Normalized upstream capability names a catalog row carried (e.g. `"tools"`). */
139
+ capabilities?: readonly string[];
140
+ /** Provider-level parallel tool call opt-in for this model. */
141
+ parallelToolCalls?: boolean;
142
+ }
143
+
144
+ /**
145
+ * Tool support a canonical source positively asserts, or undefined when nothing is known.
146
+ *
147
+ * Mirrors the routing reader (`src/routing/capability.ts`): a catalog row without `"tools"`
148
+ * is UNKNOWN, never a negative — the row may simply not enumerate capabilities. `native`
149
+ * rows and the parallel-call opt-in are positive evidence on their own.
150
+ */
151
+ export function knownToolsSupport(evidence: ExportCapabilityEvidence): boolean | undefined {
152
+ if (evidence.native === true) return true;
153
+ if (evidence.capabilities?.includes("tools") === true) return true;
154
+ if (evidence.parallelToolCalls === true) return true;
155
+ return undefined;
156
+ }
157
+
158
+ /**
159
+ * Reasoning support a canonical source positively asserts, or undefined when nothing is
160
+ * known.
161
+ *
162
+ * Never `false` from a missing or empty effort ladder: an empty ladder says "no ADJUSTABLE
163
+ * effort", which is not the claim "cannot reason" — a model can reason at a fixed depth with
164
+ * no rung to select. Known evidence is a positive reasoning rung (the catalog's own statement
165
+ * that the model accepts reasoning parameters) or delivered reasoning summaries. `none` is
166
+ * the off sentinel, not a rung: a ladder of only `none` offers an off variant and asserts
167
+ * nothing about whether the model reasons, so it is not positive evidence on its own.
168
+ */
169
+ export function knownReasoningSupport(evidence: ExportCapabilityEvidence): boolean | undefined {
170
+ if (evidence.supportsReasoningSummaries === true) return true;
171
+ if (evidence.reasoningEfforts !== undefined
172
+ && canonicalizeReasoningEfforts(evidence.reasoningEfforts).some(effort => effort !== "none")) {
173
+ return true;
174
+ }
175
+ return undefined;
80
176
  }
81
177
 
82
178
  export interface ExportContext {
@@ -88,8 +184,12 @@ export interface ExportContext {
88
184
  * admission from `apiKey` to the `x-opencodex-api-key` header.
89
185
  */
90
186
  config?: OcxConfig;
187
+ droidReasoningDefaults?: DroidReasoningDefaults;
91
188
  }
92
189
 
190
+ /** Namespaced model selector to one of that model's declared reasoning efforts. */
191
+ export type DroidReasoningDefaults = Record<string, string>;
192
+
93
193
  export type ExportClientId =
94
194
  | "opencode"
95
195
  | "pi"
@@ -1,9 +1,11 @@
1
1
  import { win32, join } from "node:path";
2
2
  import { homedir } from "node:os";
3
3
  import { exportPresentationLabel } from "../model-presentation";
4
- import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
4
+ import type { DroidReasoningDefaults, ExportContext, ExportModel, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
5
5
  import { normalizeExportModels } from "./model-metadata";
6
- import { formatSelectorConjunction } from "../../integrations/merge";
6
+ import { formatSelectorConjunction, readPath } from "../../integrations/merge";
7
+
8
+ import { DROID_DEFAULT_EFFORT_HEADER } from "./contracts";
7
9
 
8
10
  /** Factory personal settings: https://docs.factory.ai/model-independence/byok */
9
11
  export interface DroidModelEntry {
@@ -12,6 +14,7 @@ export interface DroidModelEntry {
12
14
  baseUrl: string;
13
15
  provider: "generic-chat-completion-api";
14
16
  noImageSupport: boolean;
17
+ extraHeaders?: Record<string, string>;
15
18
  }
16
19
 
17
20
  export interface DroidGeneratedConfig { customModels: DroidModelEntry[] }
@@ -43,11 +46,60 @@ function buildDroidRows(ctx: ExportContext): Array<{ row: DroidModelEntry; selec
43
46
  baseUrl: ctx.baseUrl,
44
47
  provider: "generic-chat-completion-api",
45
48
  noImageSupport: !model.inputModalities?.includes("image"),
49
+ ...(ctx.droidReasoningDefaults && Object.hasOwn(ctx.droidReasoningDefaults, model.namespaced)
50
+ ? { extraHeaders: { [DROID_DEFAULT_EFFORT_HEADER]: ctx.droidReasoningDefaults[model.namespaced]! } }
51
+ : {}),
46
52
  } });
47
53
  }
48
54
  return rows;
49
55
  }
50
56
 
57
+ export function droidReasoningModels(models: readonly ExportModel[]): Array<{ model: string; label: string; efforts: string[] }> {
58
+ const effortsByModel = new Map(normalizeExportModels(models).map(model => [model.namespaced, model.reasoningEfforts ?? []]));
59
+ return buildDroidRows({ baseUrl: "", models }).map(({ row }) => ({
60
+ model: row.model,
61
+ label: row.displayName.replace(/^OpenCodex: /, ""),
62
+ efforts: effortsByModel.get(row.model) ?? [],
63
+ }));
64
+ }
65
+
66
+ export function droidDefaultsFromOwnedRows(
67
+ ctx: ExportContext,
68
+ document: unknown,
69
+ fragmentPaths: readonly (readonly string[])[],
70
+ ): DroidReasoningDefaults {
71
+ const exportableModels = new Map(droidReasoningModels(ctx.models).map(model => [model.model, model.efforts]));
72
+ const defaults: DroidReasoningDefaults = {};
73
+ for (const path of fragmentPaths) {
74
+ if (path.length !== 2 || path[0] !== "customModels") continue;
75
+ const value = readPath(document, path);
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
77
+ const current = value as Record<string, unknown>;
78
+ if (typeof current.model !== "string" || !exportableModels.has(current.model)) continue;
79
+ const headers = current.extraHeaders;
80
+ if (!headers || typeof headers !== "object" || Array.isArray(headers)) continue;
81
+ const header = headers as Record<string, unknown>;
82
+ const effort = header[DROID_DEFAULT_EFFORT_HEADER];
83
+ if (typeof effort !== "string" || !exportableModels.get(current.model)?.includes(effort)) continue;
84
+ defaults[current.model] = effort;
85
+ }
86
+ return defaults;
87
+ }
88
+
89
+ export function validateDroidReasoningDefaults(
90
+ models: readonly ExportModel[],
91
+ defaults: unknown,
92
+ ): string | null {
93
+ if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) return "droidReasoningDefaults must be an object";
94
+ const addressable = new Map(droidReasoningModels(models).map(model => [model.model, model.efforts]));
95
+ for (const [model, effort] of Object.entries(defaults)) {
96
+ const efforts = addressable.get(model);
97
+ if (!efforts) return `model ${model} is not currently exportable to Droid`;
98
+ if (typeof effort !== "string" || !efforts.includes(effort)) return `effort for ${model} is not declared by that model`;
99
+ }
100
+ return null;
101
+ }
102
+
51
103
  export function buildDroidClientConfig(ctx: ExportContext): DroidGeneratedConfig {
52
104
  const rows = buildDroidRows(ctx);
53
105
  if (ctx.models.length > 0 && rows.length === 0) {
@@ -14,9 +14,11 @@ import {
14
14
  normalizeExportModels,
15
15
  opencodeModelCapabilities,
16
16
  outputBudgetFor,
17
+ inputBudgetFor,
17
18
  proxyAdmissionHeaders,
18
19
  singleFragment,
19
20
  } from "./model-metadata";
21
+ import { legacyReasoningMetadata, type LegacyEffortVariant } from "./reasoning-metadata";
20
22
 
21
23
  export const KILO_CONFIG_CANDIDATES = [
22
24
  "kilo.jsonc",
@@ -28,9 +30,14 @@ export const KILO_CONFIG_CANDIDATES = [
28
30
 
29
31
  export interface KiloModelEntry {
30
32
  name: string;
31
- limit?: { context: number; output: number };
33
+ limit?: { context: number; input?: number; output: number };
32
34
  attachment?: boolean;
33
35
  modalities?: { input: string[]; output: string[] };
36
+ tool_call?: boolean;
37
+ reasoning?: boolean;
38
+ interleaved?: { field: "reasoning_content" };
39
+ options?: { reasoningEffort: string };
40
+ variants?: Record<string, LegacyEffortVariant>;
34
41
  }
35
42
 
36
43
  export interface KiloProviderBlock {
@@ -77,11 +84,14 @@ function kiloProviderBlock(ctx: ExportContext): KiloProviderBlock {
77
84
  const config = ctx.config ?? OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG;
78
85
  const models: Record<string, KiloModelEntry> = {};
79
86
  for (const model of normalizeExportModels(ctx.models)) {
80
- const entry: KiloModelEntry = { name: exportModelLabel(model) };
87
+ const entry: KiloModelEntry = { name: exportModelLabel(model), ...legacyReasoningMetadata(model) };
81
88
  const context = authoritativeContextWindow(model.contextWindow);
82
89
  if (context !== undefined) {
83
90
  entry.limit = { context, output: outputBudgetFor(context, model) };
91
+ const input = inputBudgetFor(context, model);
92
+ if (input !== undefined) entry.limit.input = input;
84
93
  }
94
+ if (typeof model.supportsTools === "boolean") entry.tool_call = model.supportsTools;
85
95
  const capabilities = opencodeModelCapabilities(model.inputModalities);
86
96
  if (capabilities) {
87
97
  entry.attachment = capabilities.attachment;
@@ -29,6 +29,12 @@ export function outputBudgetFor(context: number, model: OpencodeCatalogModel): n
29
29
  return Math.min(limit, context);
30
30
  }
31
31
 
32
+ /** Input limits are optional: unlike output, a missing limit needs no schema fallback. */
33
+ export function inputBudgetFor(context: number, model: OpencodeCatalogModel): number | undefined {
34
+ const limit = authoritativeContextWindow(model.maxInputTokens);
35
+ return limit === undefined ? undefined : Math.min(limit, context);
36
+ }
37
+
32
38
  /**
33
39
  * Modalities a given client's schema will actually accept.
34
40
  *
@@ -89,12 +95,12 @@ export function inputModalitiesForClient(
89
95
  const OPENCODE_INPUT_MODALITIES: ReadonlySet<string> = new Set(["text", "audio", "image", "video", "pdf"]);
90
96
 
91
97
  /**
92
- * opencode's per-model capability fields for one catalog row, or `undefined` when the row
98
+ * Legacy per-model modality fields for one catalog row, or `undefined` when the row
93
99
  * declares nothing.
94
100
  *
95
- * `undefined` rather than `{ input: ["text"] }`: opencode already computes an entry without
96
- * capabilities as text-only, and leaving the keys out keeps every model that declares
97
- * nothing byte-identical to what shipped before. A declared list is carried across as-is, so
101
+ * `undefined` rather than guessing `{ input: ["text"] }`: unknown capabilities leave the
102
+ * client's fallback assumptions in place. V2 translates these lists into native capability
103
+ * fields; legacy clients also receive the attachment flag. A declared list is carried across as-is, so
98
104
  * an audio-only row keeps `attachment: true` instead of being rewritten to text it cannot
99
105
  * read — the same call Pi's exporter makes, in the opposite direction.
100
106
  */
@@ -0,0 +1,47 @@
1
+ import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort } from "../../reasoning-effort";
2
+ import { knownReasoningSupport, type OpencodeCatalogModel } from "./contracts";
3
+
4
+ /** Unknown and explicitly empty ladders have different client-side meanings. */
5
+ export function exportReasoningEfforts(model: OpencodeCatalogModel): string[] | undefined {
6
+ return model.reasoningEfforts === undefined ? undefined : canonicalizeReasoningEfforts(model.reasoningEfforts);
7
+ }
8
+
9
+ /** Carry an authoritative default, never invent one or revive a cleared ladder. */
10
+ export function exportDefaultReasoningEffort(model: OpencodeCatalogModel): string | undefined {
11
+ const effort = model.defaultReasoningEffort;
12
+ if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) return undefined;
13
+ const efforts = exportReasoningEfforts(model);
14
+ return efforts === undefined || efforts.includes(effort) ? effort : undefined;
15
+ }
16
+
17
+ export type LegacyEffortVariant = { reasoningEffort: string } | { disabled: true };
18
+
19
+ /** Shared V1-family controls. A disabled-only map suppresses client-invented effort ladders. */
20
+ export function legacyReasoningMetadata(model: OpencodeCatalogModel): {
21
+ reasoning?: boolean;
22
+ options?: { reasoningEffort: string };
23
+ interleaved?: { field: "reasoning_content" };
24
+ variants?: Record<string, LegacyEffortVariant>;
25
+ } {
26
+ const efforts = exportReasoningEfforts(model);
27
+ const reasoning = model.supportsReasoning ?? knownReasoningSupport({
28
+ ...model, reasoningEfforts: efforts,
29
+ });
30
+ const defaultEffort = exportDefaultReasoningEffort(model);
31
+ // Both legacy runtimes merge generated variants with config, then filter disabled IDs.
32
+ // An unrelated sentinel leaves their generated choices alive. Disable the actual IDs
33
+ // instead (live-tested on Kilo 7.8.3, checked against stock OpenCode V1's merge).
34
+ const suppressed: Record<string, LegacyEffortVariant> = Object.fromEntries(
35
+ ["none", "minimal", "low", "medium", "high", "xhigh", "max"].map(id => [id, { disabled: true }]),
36
+ );
37
+ const variants = efforts?.length
38
+ ? Object.fromEntries(efforts.map(effort => [effort, { reasoningEffort: effort }]))
39
+ : reasoning === true ? suppressed : undefined;
40
+ return {
41
+ ...(typeof reasoning === "boolean" ? { reasoning } : {}),
42
+ ...(defaultEffort !== undefined ? { options: { reasoningEffort: defaultEffort } } : {}),
43
+ ...(reasoning === true || model.supportsReasoningSummaries === true
44
+ ? { interleaved: { field: "reasoning_content" } } : {}),
45
+ ...(variants !== undefined ? { variants } : {}),
46
+ };
47
+ }