@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -286,15 +286,18 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
286
286
  }
287
287
 
288
288
  /**
289
- * Stable short account id. MUST be deterministic for a given credential: legacy
290
- * single-credential stores are re-normalized on EVERY load without being persisted,
289
+ * Stable collision-resistant account id. MUST be deterministic for a given credential:
290
+ * legacy single-credential stores are re-normalized on EVERY load without being persisted,
291
291
  * so a time-salted id would differ between two loads (getAccountSet vs
292
292
  * getAccountCredential), surfacing as a spurious OAuthLoginRequiredError and making
293
293
  * refresh persists silently miss the account (rotated refresh token lost).
294
+ *
295
+ * Keep 128 bits of SHA-256 rather than the historical 32-bit prefix. Existing persisted
296
+ * account ids are read as-is; only newly-derived ids and legacy normalization use this width.
294
297
  */
295
298
  function newAccountId(cred: OAuthCredentials): string {
296
299
  const identity = cred.accountId ?? cred.email ?? cred.refresh;
297
- return createHash("sha256").update(identity).digest("hex").slice(0, 8);
300
+ return createHash("sha256").update(identity).digest("hex").slice(0, 32);
298
301
  }
299
302
 
300
303
  function normalizeAccount(value: unknown): ProviderAccount | null {
@@ -523,6 +526,56 @@ export async function saveCredential(
523
526
  }, [provider, safe]);
524
527
  }
525
528
 
529
+ /**
530
+ * Atomically insert or replace an identity-bearing credential and report the disposition.
531
+ * Importers use this instead of a read-then-save pair so duplicate detection and persistence
532
+ * happen inside the existing serialized temp-then-rename store mutation.
533
+ */
534
+ export async function upsertCredentialByIdentity(
535
+ provider: string,
536
+ cred: OAuthCredentials,
537
+ ): Promise<"inserted" | "updated"> {
538
+ const safe = normalizeCredential(cred);
539
+ if (!safe || (!safe.accountId && !safe.email)) {
540
+ throw new Error("Refusing to persist OAuth credential without verified identity");
541
+ }
542
+ return await mutateStore(store => {
543
+ const set = store[provider];
544
+ const matches = (account: ProviderAccount): boolean => {
545
+ if (safe.accountId) {
546
+ if (account.credential.accountId) return account.credential.accountId === safe.accountId;
547
+ return Boolean(
548
+ safe.email
549
+ && account.credential.email
550
+ && account.credential.email.toLowerCase() === safe.email.toLowerCase(),
551
+ );
552
+ }
553
+ if (account.credential.accountId) return false;
554
+ return Boolean(
555
+ safe.email
556
+ && account.credential.email
557
+ && account.credential.email.toLowerCase() === safe.email.toLowerCase(),
558
+ );
559
+ };
560
+ const existing = set?.accounts.find(matches);
561
+ if (existing && set) {
562
+ existing.credential = safe;
563
+ delete existing.needsReauth;
564
+ set.activeAccountId ??= existing.id;
565
+ return "updated";
566
+ }
567
+ const id = newAccountId(safe);
568
+ const account: ProviderAccount = { id, credential: safe, addedAt: Date.now() };
569
+ if (set) {
570
+ set.accounts.push(account);
571
+ set.activeAccountId ??= id;
572
+ } else {
573
+ store[provider] = { activeAccountId: id, accounts: [account] };
574
+ }
575
+ return "inserted";
576
+ }, [provider, safe]);
577
+ }
578
+
526
579
  /** Remove the ACTIVE account; remaining accounts promote the first one. */
527
580
  export async function removeCredential(provider: string): Promise<void> {
528
581
  await mutateStore(store => {
@@ -2,6 +2,7 @@ import { codexPlanKey, codexPlanValue } from "../codex/plan";
2
2
 
3
3
  export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
4
4
  plus: 1,
5
+ team: 1,
5
6
  business: 1,
6
7
  prolite: 5,
7
8
  pro: 20,
@@ -32,6 +32,11 @@ export type InboundWire = "responses" | "chat" | "anthropic";
32
32
  */
33
33
  export type ModelWireDefault = string | { wire: string; inbound: readonly InboundWire[] };
34
34
 
35
+ export interface ResponsesTerminalRepairPolicy {
36
+ /** Quiet time after a structurally complete output graph before synthesizing completion. */
37
+ graceMs: number;
38
+ }
39
+
35
40
  export type ProviderModelDiscoveryScalar = string | number | boolean;
36
41
 
37
42
  export type ProviderModelDiscoveryPredicate =
@@ -162,6 +167,8 @@ export interface ProviderRegistryEntry {
162
167
  * can omit or indefinitely delay the terminal event.
163
168
  */
164
169
  modelResponsesUpstreamStreaming?: Record<string, boolean>;
170
+ /** Registry-only repair for a model whose native Responses stream may omit its terminal. */
171
+ modelResponsesTerminalRepair?: Record<string, ResponsesTerminalRepairPolicy>;
165
172
  /**
166
173
  * Registry-only client-facing item-id repair policy (#938), filled onto the
167
174
  * runtime provider only when the user has no explicit policy (derive.ts);
@@ -187,8 +194,8 @@ export interface ProviderRegistryEntry {
187
194
  */
188
195
  statelessResponses?: boolean;
189
196
  /**
190
- * Responses parser requires a matched tool result directly after its call. This is
191
- * seeded/backfilled like other fixed upstream wire-contract capabilities.
197
+ * Responses parser requires an unambiguous call batch and its matched result batch
198
+ * to stay contiguous. This is seeded/backfilled like other fixed wire capabilities.
192
199
  */
193
200
  requiresAdjacentResponsesToolResults?: boolean;
194
201
  /**
@@ -293,12 +300,6 @@ const MINIMAX_M3_REASONING_EFFORT_MAP: Record<string, string> = {
293
300
  const OPENAI_GPT56_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"];
294
301
  const OPENAI_GPT56_PRO_MODELS = ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"];
295
302
  const OPENAI_API_GPT56_CONTEXT_WINDOW = 1_050_000;
296
- const OPENAI_CODEX_GPT56_CONTEXT_WINDOW = 372_000;
297
- const OPENAI_GPT56_CONTEXT_WINDOWS = {
298
- "gpt-5.6-sol": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
299
- "gpt-5.6-terra": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
300
- "gpt-5.6-luna": OPENAI_CODEX_GPT56_CONTEXT_WINDOW,
301
- };
302
303
  const OPENAI_API_GPT56_CONTEXT_WINDOWS: Record<string, number> = {
303
304
  ...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_CONTEXT_WINDOW])),
304
305
  "gpt-5.5": OPENAI_API_GPT56_CONTEXT_WINDOW,
@@ -313,6 +314,37 @@ const OPENAI_API_GPT56_VIRTUAL_MODELS: Record<string, { wireModelId: string; rea
313
314
  "gpt-5.6-luna-pro": { wireModelId: "gpt-5.6-luna", reasoningMode: "pro" },
314
315
  };
315
316
  const OPENAI_API_GPT56_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
317
+ /**
318
+ * Daybreak program aliases. These `-latest` ids are the stable contract: OpenAI repoints
319
+ * them at newer snapshots over time (red -> gpt-5.6-cyber, blue -> gpt-5.6-sol as of
320
+ * 2026-08-11), so registering the ALIAS inherits future model swaps while a pinned
321
+ * snapshot id would silently go stale. Snapshot ids are deliberately absent here.
322
+ * Responses-only per both published endpoint tables (`v1/chat/completions` is marked
323
+ * Not supported) — never add these to a chat-completions provider. Access needs separate
324
+ * Daybreak approval and provisioning, so neither is ever a default.
325
+ * Verified 2026-08-11: developers.openai.com/api/docs/models/daybreak-red-latest.md
326
+ * and .../daybreak-blue-latest.md
327
+ */
328
+ const OPENAI_DAYBREAK_MODELS = ["daybreak-red-latest", "daybreak-blue-latest"];
329
+ const OPENAI_DAYBREAK_CONTEXT_WINDOWS: Record<string, number> = {
330
+ "daybreak-red-latest": 400_000,
331
+ "daybreak-blue-latest": 1_050_000,
332
+ };
333
+ const OPENAI_DAYBREAK_MAX_INPUT_TOKENS: Record<string, number> = {
334
+ "daybreak-red-latest": 272_000,
335
+ "daybreak-blue-latest": 922_000,
336
+ };
337
+ /**
338
+ * Neither Daybreak page publishes a reasoning-effort ladder. An explicit empty array means
339
+ * "expose no effort control"; OMITTING the key would instead fall back to the full routed
340
+ * ladder (`configuredReasoningEfforts` returns undefined -> `applyReasoningLevels` uses
341
+ * ROUTED_REASONING_LEVELS), which would advertise efforts the models never documented.
342
+ * `noReasoningModels` is wrong here: both pages document reasoning-token support, so these
343
+ * are reasoning models with no *selectable* ladder.
344
+ */
345
+ const OPENAI_DAYBREAK_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
346
+ OPENAI_DAYBREAK_MODELS.map(id => [id, [] as string[]]),
347
+ );
316
348
  const OPENROUTER_GPT56_MODELS = OPENAI_GPT56_MODELS.map(id => `openai/${id}`);
317
349
  // OpenRouter's live /endpoints routes report 1,050,000; keep this separate from the
318
350
  // unverified OpenAI API-key seed. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
@@ -1052,6 +1084,45 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1052
1084
  modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
1053
1085
  modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
1054
1086
  },
1087
+ {
1088
+ // Nous Portal — Nous Research subscription gateway (same backend Hermes Agent
1089
+ // uses). OAuth is a device grant (src/oauth/nous.ts): the access token IS the
1090
+ // per-request inference JWT (scope inference:invoke), refresh tokens are
1091
+ // single-use and rotated on every refresh. Catalog is a mix of paid models
1092
+ // (billed against the Portal subscription) and `:free` slugs (e.g.
1093
+ // tencent/hy3:free, stepfun/step-3.7-flash:free, inclusionai/ling-3.0-flash:free);
1094
+ // free-tier gating is decided live by the Portal per account, so discovery
1095
+ // from the signed-in account is authoritative; the static seed below is the
1096
+ // logged-out fallback and only lists free models verified on a real account
1097
+ // (2026-08-10): the Portal free list is authoritative and currently has
1098
+ // exactly 4 :free models: tencent/hy3:free, poolside/laguna-s-2.1:free,
1099
+ // stepfun/step-3.7-flash:free, poolside/laguna-xs-2.1:free.
1100
+ // inclusionai/ling-3.0-flash:free was removed from the Portal free list
1101
+ // (404 on the inference API since 2026-08-07) and must not be seeded.
1102
+ id: "nous",
1103
+ label: "Nous Portal",
1104
+ adapter: "openai-chat",
1105
+ baseUrl: "https://inference-api.nousresearch.com/v1",
1106
+ authKind: "oauth",
1107
+ oauthId: "nous",
1108
+ featured: true,
1109
+ // Mixed free + paid provider: the free tier is per-model (the `:free`
1110
+ // slugs), not a property of the whole provider, so freeTier stays false to
1111
+ // avoid implying every model is free.
1112
+ freeTier: false,
1113
+ dashboardUrl: "https://portal.nousresearch.com",
1114
+ defaultModel: "tencent/hy3:free",
1115
+ liveModels: true,
1116
+ models: ["tencent/hy3:free", "poolside/laguna-s-2.1:free", "stepfun/step-3.7-flash:free", "poolside/laguna-xs-2.1:free"],
1117
+ modelDiscovery: {
1118
+ // Resolves against effectiveBaseUrl (registry baseUrl .../v1) to the same
1119
+ // canonical endpoint https://inference-api.nousresearch.com/v1/models.
1120
+ path: "models",
1121
+ maxResponseBytes: 262_144,
1122
+ maxModels: 512,
1123
+ },
1124
+ note: "Nous Research subscription gateway. OAuth device login with your own Portal account; mixed paid + :free models discovered live (fallback seed 2026-08-10: tencent/hy3:free, poolside/laguna-s-2.1:free, stepfun/step-3.7-flash:free, poolside/laguna-xs-2.1:free).",
1125
+ },
1055
1126
  {
1056
1127
  id: "openai-apikey",
1057
1128
  label: "OpenAI API",
@@ -1062,16 +1133,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1062
1133
  featured: true,
1063
1134
  dashboardUrl: "https://platform.openai.com/api-keys",
1064
1135
  defaultModel: "gpt-5.5",
1065
- models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS],
1136
+ models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS],
1066
1137
  liveModels: true,
1067
- modelContextWindows: OPENAI_API_GPT56_CONTEXT_WINDOWS,
1068
- modelMaxInputTokens: OPENAI_API_GPT56_MAX_INPUT_TOKENS,
1138
+ modelContextWindows: { ...OPENAI_API_GPT56_CONTEXT_WINDOWS, ...OPENAI_DAYBREAK_CONTEXT_WINDOWS },
1139
+ modelMaxInputTokens: { ...OPENAI_API_GPT56_MAX_INPUT_TOKENS, ...OPENAI_DAYBREAK_MAX_INPUT_TOKENS },
1069
1140
  modelInputModalities: Object.fromEntries(
1070
- ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, ["text", "image"]]),
1071
- ),
1072
- modelReasoningEfforts: Object.fromEntries(
1073
- [...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]),
1141
+ ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS]
1142
+ .map(id => [id, ["text", "image"]]),
1074
1143
  ),
1144
+ modelReasoningEfforts: {
1145
+ ...Object.fromEntries(
1146
+ [...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_REASONING_EFFORTS]),
1147
+ ),
1148
+ ...OPENAI_DAYBREAK_REASONING_EFFORTS,
1149
+ },
1075
1150
  virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
1076
1151
  },
1077
1152
  {
@@ -1102,6 +1177,15 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1102
1177
  id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
1103
1178
  authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
1104
1179
  jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
1180
+ /* [Decision Log]
1181
+ - 목적과 의도: Route GPT 5.6 Luna to the Responses endpoint that OpenCode Go documents for that exact model.
1182
+ - 기존 구현 및 제약 조건: The provider is mixed-wire but its provider-wide `openai-chat` adapter sent Luna to `/chat/completions`; explicit user `modelAdapters` entries must remain authoritative.
1183
+ - 검토한 주요 대안: Change the whole provider to Responses; infer the wire from model-family names; add one registry-only exact-model default.
1184
+ - 선택한 방식: Declare only `gpt-5.6-luna` as `openai-responses` through the existing registry default mechanism.
1185
+ - 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence.
1186
+ - 장점, 단점 및 영향: Luna reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update.
1187
+ */
1188
+ modelWireDefaults: { "gpt-5.6-luna": "openai-responses" },
1105
1189
  modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW },
1106
1190
  modelInputModalities: { "kimi-k3": ["text", "image"] },
1107
1191
  modelReasoningEfforts: {
@@ -1352,6 +1436,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1352
1436
  // for providers that need it — re-adding one line here restores the old policy.
1353
1437
  // Evidence: https://api-docs.deepseek.com/guides/responses_api/ +
1354
1438
  // devlog/_plan/260807_deepseek_responses_streaming/000_plan.md.
1439
+ // Current official streams normally carry a real terminal; retain a narrow grace
1440
+ // repair for the historical shape that closes after a complete graph without one.
1441
+ modelResponsesTerminalRepair: { "deepseek-v4-flash": { graceMs: 5_000 } },
1355
1442
  // DeepSeek's Responses route emits bare UUID item ids, which leave Codex
1356
1443
  // clients stuck on an uncommitted turn (#938). Client-facing only — raw
1357
1444
  // continuation snapshots keep the upstream ids.
@@ -1375,7 +1462,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1375
1462
  // server." https://api-docs.deepseek.com/api/create-response/
1376
1463
  statelessResponses: true,
1377
1464
  // DeepSeek rejects a valid Codex continuation when hook-provided developer
1378
- // context is persisted between a call and its matching result (#1292).
1465
+ // context splits a call from its result (#1292); parallel calls remain one
1466
+ // reasoning-bearing assistant batch rather than being split per pair (#1477).
1379
1467
  requiresAdjacentResponsesToolResults: true,
1380
1468
  /* [Decision Log]
1381
1469
  - 목적: DeepSeek V4 thinking mode multi-turn/tool-call requests must replay prior assistant reasoning_content.
@@ -1720,6 +1808,49 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1720
1808
  },
1721
1809
  note: "Authenticated first page of popular chat models only; live discovery admits at most 100 plan-available, ungated rows whose metadata explicitly reports tool use.",
1722
1810
  },
1811
+ {
1812
+ // Primary sources checked 2026-08-08:
1813
+ // - https://novita.ai/docs/api-reference/model-apis-llm-create-chat-completion and
1814
+ // https://novita.ai/docs/api-reference/model-apis-llm-list-models document the fixed
1815
+ // OpenAI-compatible Chat Completions and model-list endpoints.
1816
+ // - https://novita.ai/docs/api-reference/basic-authentication documents Bearer API keys.
1817
+ // - https://novita.ai/legal/terms-of-service (updated 2026-08-05) expressly covers AI
1818
+ // inference APIs, third-party Model Providers, and customer Input/Output processing.
1819
+ // - https://huggingface.co/docs/inference-providers/main/providers/novita lists Novita as an
1820
+ // Inference Providers partner for chat/VLM traffic, independently supporting routing use.
1821
+ // - https://tsdr.uspto.gov/statusview/sn99255805 is the official use-in-commerce record
1822
+ // connecting the NOVITA AI mark to Hivemind Labs, Inc., a Delaware corporation. The mark
1823
+ // application is now abandoned; it is cited only as the public operator-identity record.
1824
+ // Maintainer: @olddonkey; no affiliation with Novita AI or Hivemind Labs, Inc.
1825
+ id: "novita",
1826
+ label: "Novita AI",
1827
+ baseUrl: "https://api.novita.ai/openai/v1",
1828
+ adapter: "openai-chat",
1829
+ authKind: "key",
1830
+ dashboardUrl: "https://novita.ai/settings/key-management",
1831
+ liveModels: true,
1832
+ preserveCustomDestination: true,
1833
+ // The live catalog is public even though the reference shows an Authorization header, so a
1834
+ // successful model fetch cannot prove that a supplied key is valid.
1835
+ apiKeyValidation: "unknown",
1836
+ // The request reference documents tools but not a provider-wide parallel-tool contract.
1837
+ parallelToolCalls: false,
1838
+ // Novita exposes model-specific thinking flags, not an OpenAI reasoning_effort contract.
1839
+ reasoningEfforts: [],
1840
+ modelDiscovery: {
1841
+ path: "models",
1842
+ maxResponseBytes: 512 * 1024,
1843
+ maxModels: 256,
1844
+ filter: {
1845
+ // Require both Novita's chat classification and the exact configured wire endpoint.
1846
+ allOf: [
1847
+ { path: ["model_type"], equalsAny: ["chat"] },
1848
+ { path: ["endpoints"], containsAny: ["chat/completions"] },
1849
+ ],
1850
+ },
1851
+ },
1852
+ note: "Public live catalog filtered to rows that explicitly report chat type and Chat Completions support; key validity remains unknown until an authenticated inference request.",
1853
+ },
1723
1854
  // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
1724
1855
  { id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
1725
1856
  { id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
@@ -2380,6 +2511,20 @@ export function providerModelResponsesUpstreamStreaming(
2380
2511
  return entry.modelResponsesUpstreamStreaming[modelId.trim().toLowerCase()];
2381
2512
  }
2382
2513
 
2514
+ /** Resolve a registry-only terminal-repair policy for native Responses streams. */
2515
+ export function providerModelResponsesTerminalRepair(
2516
+ id: string,
2517
+ provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
2518
+ modelId: string,
2519
+ ): ResponsesTerminalRepairPolicy | undefined {
2520
+ const entry = getProviderRegistryEntry(id);
2521
+ if (!entry?.modelResponsesTerminalRepair || !providerMatchesRegistryTransport(id, provider)) return undefined;
2522
+ const policy = entry.modelResponsesTerminalRepair[modelId.trim().toLowerCase()];
2523
+ const graceMs = Math.floor(policy?.graceMs ?? 0);
2524
+ if (!Number.isFinite(graceMs) || graceMs <= 0) return undefined;
2525
+ return { graceMs };
2526
+ }
2527
+
2383
2528
  /**
2384
2529
  * Effective Codex account mode for a provider. For canonical `openai`, a valid persisted
2385
2530
  * `codexAccountMode` on the provider config wins and a missing/invalid value defaults to
@@ -0,0 +1,182 @@
1
+ const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
2
+
3
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
4
+ return !!value && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ function customToolInput(argumentsText: unknown): string {
8
+ if (typeof argumentsText !== "string") return "";
9
+ try {
10
+ const parsed = JSON.parse(argumentsText) as unknown;
11
+ if (isPlainObject(parsed) && typeof parsed.input === "string") return parsed.input;
12
+ } catch { /* malformed arguments stay visible to the client */ }
13
+ return argumentsText;
14
+ }
15
+
16
+ export function customToolItemId(id: unknown): unknown {
17
+ if (typeof id !== "string") return id;
18
+ return id.startsWith("fc_") ? `ctc_${id.slice(3)}` : id;
19
+ }
20
+
21
+ export function collectRoutedCustomToolNames(body: unknown): Set<string> {
22
+ const names = new Set<string>();
23
+ const visit = (value: unknown): void => {
24
+ if (Array.isArray(value)) {
25
+ for (const entry of value) visit(entry);
26
+ return;
27
+ }
28
+ if (!isPlainObject(value)) return;
29
+ if (
30
+ value.type === "custom"
31
+ && typeof value.name === "string"
32
+ && !ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(value.name)
33
+ ) {
34
+ names.add(value.name);
35
+ }
36
+ for (const entry of Object.values(value)) visit(entry);
37
+ };
38
+ visit(body);
39
+ return names;
40
+ }
41
+
42
+ function collectConvertedCallIds(value: unknown, names: ReadonlySet<string>, out: Set<string>): void {
43
+ if (Array.isArray(value)) {
44
+ for (const entry of value) collectConvertedCallIds(entry, names, out);
45
+ return;
46
+ }
47
+ if (!isPlainObject(value)) return;
48
+ if (
49
+ (value.type === "custom_tool_call" || value.type === "function_call")
50
+ && typeof value.name === "string"
51
+ && names.has(value.name)
52
+ && typeof value.call_id === "string"
53
+ ) {
54
+ out.add(value.call_id);
55
+ }
56
+ for (const entry of Object.values(value)) collectConvertedCallIds(entry, names, out);
57
+ }
58
+
59
+ function rewriteForUpstream(
60
+ value: unknown,
61
+ names: ReadonlySet<string>,
62
+ callIds: ReadonlySet<string>,
63
+ ): unknown {
64
+ if (Array.isArray(value)) return value.map(entry => rewriteForUpstream(entry, names, callIds));
65
+ if (!isPlainObject(value)) return value;
66
+
67
+ if (value.type === "custom" && typeof value.name === "string" && names.has(value.name)) {
68
+ const { format: _format, ...rest } = value;
69
+ const isDefinition = typeof value.description === "string"
70
+ || isPlainObject(value.format)
71
+ || isPlainObject(value.parameters);
72
+ if (!isDefinition) return { ...rest, type: "function" };
73
+ return {
74
+ ...rest,
75
+ type: "function",
76
+ parameters: {
77
+ type: "object",
78
+ properties: {
79
+ input: {
80
+ type: "string",
81
+ description: "Raw input for this client-executed custom tool.",
82
+ },
83
+ },
84
+ required: ["input"],
85
+ additionalProperties: false,
86
+ },
87
+ };
88
+ }
89
+
90
+ if (
91
+ value.type === "custom_tool_call"
92
+ && typeof value.name === "string"
93
+ && names.has(value.name)
94
+ ) {
95
+ const { input, id: _id, ...rest } = value;
96
+ return {
97
+ ...rest,
98
+ type: "function_call",
99
+ arguments: JSON.stringify({ input: typeof input === "string" ? input : "" }),
100
+ };
101
+ }
102
+
103
+ if (
104
+ value.type === "custom_tool_call_output"
105
+ && typeof value.call_id === "string"
106
+ && callIds.has(value.call_id)
107
+ ) {
108
+ return { ...value, type: "function_call_output" };
109
+ }
110
+
111
+ let changed = false;
112
+ const next: Record<string, unknown> = {};
113
+ for (const [key, entry] of Object.entries(value)) {
114
+ const rewritten = rewriteForUpstream(entry, names, callIds);
115
+ next[key] = rewritten;
116
+ changed ||= rewritten !== entry;
117
+ }
118
+ return changed ? next : value;
119
+ }
120
+
121
+ export function rewriteRoutedCustomToolsForUpstream(body: unknown): {
122
+ body: unknown;
123
+ names: Set<string>;
124
+ } {
125
+ const names = collectRoutedCustomToolNames(body);
126
+ if (names.size === 0) return { body, names };
127
+ const callIds = new Set<string>();
128
+ collectConvertedCallIds(body, names, callIds);
129
+ return { body: rewriteForUpstream(body, names, callIds), names };
130
+ }
131
+
132
+ export function restoreRoutedCustomCalls(
133
+ value: unknown,
134
+ names: ReadonlySet<string>,
135
+ ): { value: unknown; changed: boolean } {
136
+ if (Array.isArray(value)) {
137
+ let changed = false;
138
+ const restored = value.map(entry => {
139
+ const result = restoreRoutedCustomCalls(entry, names);
140
+ changed ||= result.changed;
141
+ return result.value;
142
+ });
143
+ return changed ? { value: restored, changed: true } : { value, changed: false };
144
+ }
145
+ if (!isPlainObject(value)) return { value, changed: false };
146
+
147
+ let changed = false;
148
+ const restored: Record<string, unknown> = {};
149
+ for (const [key, entry] of Object.entries(value)) {
150
+ const result = restoreRoutedCustomCalls(entry, names);
151
+ restored[key] = result.value;
152
+ changed ||= result.changed;
153
+ }
154
+
155
+ if (value.type === "function_call" && typeof value.name === "string" && names.has(value.name)) {
156
+ restored.type = "custom_tool_call";
157
+ restored.id = customToolItemId(value.id);
158
+ restored.input = customToolInput(value.arguments);
159
+ delete restored.arguments;
160
+ changed = true;
161
+ }
162
+ return changed ? { value: restored, changed: true } : { value, changed: false };
163
+ }
164
+
165
+ export function restoreRoutedCustomCallsInJson(
166
+ text: string,
167
+ names: ReadonlySet<string>,
168
+ ): string {
169
+ if (names.size === 0) return text;
170
+ let payload: unknown;
171
+ try {
172
+ payload = JSON.parse(text);
173
+ } catch {
174
+ return text;
175
+ }
176
+ const restored = restoreRoutedCustomCalls(payload, names);
177
+ return restored.changed ? JSON.stringify(restored.value) : text;
178
+ }
179
+
180
+ export function unwrapRoutedCustomToolArguments(argumentsText: unknown): string {
181
+ return customToolInput(argumentsText);
182
+ }
@@ -324,17 +324,33 @@ export function parseRequest(body: unknown): OcxParsedRequest {
324
324
  // synthetic `{type:"compaction"}` output item (src/responses/compaction.ts). Flagged for the server.
325
325
  let compactionRequest = false;
326
326
  let contextCompactionBoundary = false;
327
+ let continuationConversationMessageIndex: number | undefined;
327
328
 
328
329
  if (typeof data.instructions === "string" && data.instructions.length > 0) {
329
330
  systemPrompt.push(data.instructions);
330
331
  }
331
332
 
332
333
  if (typeof data.input === "string") {
334
+ if (data.previous_response_id) continuationConversationMessageIndex = messages.length;
333
335
  messages.push({ role: "user", content: data.input, timestamp: now });
334
336
  } else if (data.input) {
335
337
  for (let inputIndex = 0; inputIndex < data.input.length; inputIndex++) {
336
338
  const item = data.input[inputIndex];
337
339
  const effectiveType = (item as { type?: string }).type ?? ("role" in item ? "message" : undefined);
340
+ const itemRole = (item as { role?: string }).role;
341
+ // Raw protocol items do not map one-to-one onto context messages. Capture the boundary while
342
+ // both representations are available so later metadata can stay before conversation in both.
343
+ if (
344
+ data.previous_response_id
345
+ && inputIndex >= replayedInputPrefixLength
346
+ && continuationConversationMessageIndex === undefined
347
+ && (
348
+ effectiveType === "agent_message"
349
+ || (effectiveType === "message" && (itemRole === "user" || itemRole === "assistant"))
350
+ )
351
+ ) {
352
+ continuationConversationMessageIndex = messages.length;
353
+ }
338
354
 
339
355
  if (effectiveType === "compaction_trigger") {
340
356
  compactionRequest = true;
@@ -614,6 +630,9 @@ export function parseRequest(body: unknown): OcxParsedRequest {
614
630
  }
615
631
  }
616
632
  }
633
+ if (data.previous_response_id && continuationConversationMessageIndex === undefined) {
634
+ continuationConversationMessageIndex = messages.length;
635
+ }
617
636
 
618
637
  const declaredTools = buildTools(data.tools as unknown[] | undefined) ?? [];
619
638
  const loadedTools = buildTools(loadedToolSpecs) ?? [];
@@ -683,6 +702,9 @@ export function parseRequest(body: unknown): OcxParsedRequest {
683
702
  options,
684
703
  _rawBody: body,
685
704
  ...(replayedInputPrefixLength > 0 ? { _replayPrefixLen: replayedInputPrefixLength } : {}),
705
+ ...(continuationConversationMessageIndex !== undefined
706
+ ? { _continuationConversationMessageIndex: continuationConversationMessageIndex }
707
+ : {}),
686
708
  ...(webSearch ? { _webSearch: webSearch } : {}),
687
709
  ...(imageGen ? { _imageGeneration: imageGen } : {}),
688
710
  ...(textFormat ? { _structuredOutput: true } : {}),