@bitkyc08/opencodex 2.69.0 → 2.70.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 (147) hide show
  1. package/gui/dist/assets/App-_MrbTrBe.js +51 -0
  2. package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
  3. package/gui/dist/assets/index-DKrO2eF3.js +86 -0
  4. package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-model-contract.ts +119 -0
  8. package/src/adapters/anthropic.ts +14 -83
  9. package/src/adapters/base.ts +2 -0
  10. package/src/adapters/command-code-project-context.ts +542 -0
  11. package/src/adapters/command-code-tool-text.ts +99 -67
  12. package/src/adapters/command-code.ts +5 -1
  13. package/src/adapters/cursor/catalog.ts +11 -0
  14. package/src/adapters/cursor/effort-map.ts +3 -0
  15. package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
  16. package/src/adapters/devin/cloud-direct/chat.ts +78 -48
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
  18. package/src/adapters/devin/context-overflow.ts +75 -0
  19. package/src/adapters/devin/live-models.ts +174 -13
  20. package/src/adapters/devin/reasoning-signature.ts +100 -0
  21. package/src/adapters/devin.ts +310 -117
  22. package/src/adapters/kiro/payload.ts +10 -10
  23. package/src/adapters/kiro-images.ts +58 -7
  24. package/src/adapters/openai-chat/passthrough.ts +3 -1
  25. package/src/adapters/openai-chat.ts +2 -1
  26. package/src/adapters/openai-responses/passthrough.ts +2 -1
  27. package/src/bridge/response-json.ts +15 -1
  28. package/src/bridge/sse.ts +3 -1
  29. package/src/claude/intercept/model-bindings.ts +1 -0
  30. package/src/cli/account-extended.ts +31 -1
  31. package/src/cli/account.ts +3 -0
  32. package/src/cli/capabilities.ts +13 -0
  33. package/src/cli/client-start-fence.ts +36 -0
  34. package/src/cli/dispatch.ts +3 -2
  35. package/src/cli/index.ts +98 -117
  36. package/src/cli/provider-runtime.ts +23 -2
  37. package/src/cli/provider.ts +3 -0
  38. package/src/cli/resolve.ts +18 -0
  39. package/src/cli/start-owner-fence.ts +30 -0
  40. package/src/cli/stop-approval.ts +13 -1
  41. package/src/cli/stop-restore.ts +73 -0
  42. package/src/cli/tray-proxy.ts +221 -18
  43. package/src/cli/version-skew.ts +25 -23
  44. package/src/client/runtime.ts +8 -4
  45. package/src/codex/auth-api/account-list.ts +11 -5
  46. package/src/codex/auth-api/login-flow.ts +5 -3
  47. package/src/codex/auth-api/main-account-probe.ts +186 -110
  48. package/src/codex/auth-api/pool-mode-gate.ts +61 -7
  49. package/src/codex/auth-api/pool-quota-probe.ts +142 -90
  50. package/src/codex/auth-api/reset-credit-service.ts +1 -1
  51. package/src/codex/catalog/gather-capture.ts +1 -0
  52. package/src/codex/catalog/model-hints.ts +3 -3
  53. package/src/codex/catalog/provider-models.ts +2 -0
  54. package/src/codex/low-quota-events.ts +28 -0
  55. package/src/codex/low-quota-observer.ts +23 -0
  56. package/src/codex/low-quota-protection.ts +272 -0
  57. package/src/codex/quota-query-backoff.ts +148 -0
  58. package/src/codex/quota.ts +7 -2
  59. package/src/config/diagnostics.ts +10 -1
  60. package/src/config/load-degrade.ts +17 -0
  61. package/src/config/mutation-lock.ts +3 -4
  62. package/src/config/process-state.ts +1 -1
  63. package/src/config/provider-validation.ts +22 -0
  64. package/src/config/schema/blocked-model-redirects.ts +17 -0
  65. package/src/config/schema/config-schema.ts +4 -1
  66. package/src/config/schema/leaf-validators.ts +33 -2
  67. package/src/config/serving-runtimes.ts +412 -0
  68. package/src/generated/compatibility-version.json +211 -115
  69. package/src/generated/model-metadata.ts +3 -3
  70. package/src/images/loop.ts +3 -0
  71. package/src/lib/errors.ts +2 -1
  72. package/src/lib/handled-signal-exit.ts +23 -0
  73. package/src/lib/request-failure-model.ts +1 -0
  74. package/src/lib/self-launch-argv.ts +9 -0
  75. package/src/lib/shadow-call.ts +5 -2
  76. package/src/lib/strict-semver.ts +26 -0
  77. package/src/lib/transient-service-path.ts +16 -0
  78. package/src/lib/windows-service-wrappers.ts +10 -6
  79. package/src/lib/winsw.ts +3 -1
  80. package/src/oauth/anthropic-model-routes.ts +105 -0
  81. package/src/oauth/anthropic-routing.ts +85 -48
  82. package/src/oauth/devin.ts +135 -5
  83. package/src/oauth/index.ts +20 -3
  84. package/src/oauth/kiro-terminal-failover.ts +23 -9
  85. package/src/oauth/pool-settings-capability.ts +10 -2
  86. package/src/oauth/store.ts +1 -1
  87. package/src/protocols/encoders/adapter-events.ts +3 -1
  88. package/src/providers/github-copilot-context.ts +27 -0
  89. package/src/providers/kiro-models.ts +4 -0
  90. package/src/providers/model-rename-fields.ts +3 -0
  91. package/src/providers/quota/account-cache.ts +8 -3
  92. package/src/providers/quota/devin.ts +286 -0
  93. package/src/providers/quota/vendor-probes-oauth.ts +6 -2
  94. package/src/providers/quota.ts +1 -1
  95. package/src/providers/registry/entries-core.ts +1 -1
  96. package/src/providers/registry/model-seeds.ts +5 -2
  97. package/src/responses/compaction-images.ts +27 -0
  98. package/src/router.ts +104 -16
  99. package/src/routing/analytics.ts +1 -0
  100. package/src/server/auth-cors.ts +9 -1
  101. package/src/server/background-lifecycle.ts +21 -10
  102. package/src/server/chat-completions.ts +2 -1
  103. package/src/server/chat-native.ts +1 -0
  104. package/src/server/index.ts +4 -8
  105. package/src/server/inference/client-encoder-delivery.ts +3 -0
  106. package/src/server/management/context.ts +3 -0
  107. package/src/server/management/low-quota-routes.ts +14 -0
  108. package/src/server/management/oauth-account-routes.ts +21 -1
  109. package/src/server/management/provider-overwrite-carry.ts +79 -6
  110. package/src/server/management/provider-routes.ts +41 -28
  111. package/src/server/management/route-registry.ts +1 -0
  112. package/src/server/management-api.ts +7 -0
  113. package/src/server/responses/adapter-continuation.ts +3 -0
  114. package/src/server/responses/adapter-delivery.ts +3 -0
  115. package/src/server/responses/adapter-dispatch.ts +20 -4
  116. package/src/server/responses/antigravity-validation-refusal.ts +20 -0
  117. package/src/server/responses/core-auth.ts +1 -0
  118. package/src/server/responses/core-codex-account.ts +1 -0
  119. package/src/server/responses/core-normalize.ts +4 -0
  120. package/src/server/responses/passthrough-dispatch.ts +8 -1
  121. package/src/server/responses/request-sidecar-auth.ts +2 -0
  122. package/src/server/responses/request-transport.ts +27 -10
  123. package/src/server/responses/response-effects.ts +4 -1
  124. package/src/server/responses/run-turn-execution.ts +126 -42
  125. package/src/server/responses/sidecar-execution.ts +4 -1
  126. package/src/service/diagnostics.ts +3 -1
  127. package/src/service/guarded-manager-target.ts +39 -12
  128. package/src/service/launchd.ts +14 -6
  129. package/src/service/repair.ts +6 -1
  130. package/src/service/service-child-ownership.ts +168 -0
  131. package/src/service/state.ts +22 -4
  132. package/src/service/systemd.ts +2 -2
  133. package/src/service/windows-guarded-manager.ts +530 -0
  134. package/src/service/windows-taskxml.ts +2 -2
  135. package/src/service.ts +2 -0
  136. package/src/types/config.ts +26 -0
  137. package/src/types/provider.ts +12 -0
  138. package/src/types/request.ts +2 -0
  139. package/src/types.ts +1 -0
  140. package/src/usage/expected-prices.ts +13 -0
  141. package/src/usage/telemetry-contract.ts +1 -0
  142. package/src/vision/anthropic-describe.ts +2 -1
  143. package/src/web-search/anthropic-executor.ts +2 -1
  144. package/src/web-search/loop.ts +1 -0
  145. package/gui/dist/assets/App-DDbgOTh6.js +0 -51
  146. package/gui/dist/assets/index-CuaBJkG4.js +0 -86
  147. package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
@@ -10,13 +10,14 @@ import type { AdapterEvent, OcxAssistantMessage, OcxContentPart, OcxMessage, Ocx
10
10
  import { namespacedToolName } from "../types";
11
11
  import type { IncomingMeta, ProviderAdapter } from "./base";
12
12
  import { streamChatEventsWithResetRetry, devinStatedResetWaitMs, allocateCascadeId, CloudChatError, type ChatHistoryItem, type ToolDef } from "./devin/cloud-direct";
13
- import type { ContentPart } from "./devin/cloud-direct/chat";
14
- import { getCachedCatalog, type CacheEntry } from "./devin/cloud-direct/catalog";
15
- import { collapseDevinModelUid } from "./devin/live-models";
13
+ import type { CloudChatEvent, ContentPart } from "./devin/cloud-direct/chat";
14
+ import { getCachedCatalog, type CacheEntry, type ModelCatalogEntry } from "./devin/cloud-direct/catalog";
15
+ import { collapseDevinModelUid, devinFamiliesOf, devinFamilyBaseId, selectDevinFamilyMember, type DevinVariantRequest } from "./devin/live-models";
16
16
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
17
17
  import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
18
- import { isProviderIssuedThinkingSignature } from "../responses/reasoning-envelope";
18
+ import { devinAssistantReasoning, encodeDevinSignature, hasAnthropicSignature } from "./devin/reasoning-signature";
19
19
  import { SendBudgetExhaustedError } from "../lib/upstream-retry";
20
+ import { devinContextOverflowEvent, isDevinHistoryOverflow } from "./devin/context-overflow";
20
21
 
21
22
  /**
22
23
  * Combine two usage frames from one turn by keeping the larger count per field.
@@ -54,6 +55,49 @@ export function mergeDevinUsage(previous: OcxUsage, next: OcxUsage): OcxUsage {
54
55
  */
55
56
  const DEVIN_CLIENT_CLOSED_MESSAGE = "client closed request";
56
57
 
58
+ /** Below the bridge's upstream stall deadline, so held reasoning never reads as a stall. */
59
+ const HELD_REASONING_HEARTBEAT_MS = 15_000;
60
+ /** Once either limit is crossed, forward the signed attempt and disable fallback. */
61
+ const HELD_REASONING_MAX_EVENTS = 1_024;
62
+ const HELD_REASONING_MAX_PAYLOAD_BYTES = 1024 * 1024;
63
+
64
+ type DevinUsageEvent = Extract<CloudChatEvent, { kind: "usage" }>;
65
+
66
+ function toOcxDevinUsage(event: DevinUsageEvent): OcxUsage {
67
+ const total = event.totalTokens ?? ((event.promptTokens ?? 0) + (event.completionTokens ?? 0));
68
+ return {
69
+ inputTokens: event.promptTokens ?? 0,
70
+ outputTokens: event.completionTokens ?? 0,
71
+ ...(total > 0 ? { totalTokens: total } : {}),
72
+ ...(event.cachedInputTokens !== undefined ? { cachedInputTokens: event.cachedInputTokens } : {}),
73
+ ...(event.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: event.cacheCreationInputTokens } : {}),
74
+ ...(event.reasoningTokens !== undefined ? { reasoningOutputTokens: event.reasoningTokens } : {}),
75
+ };
76
+ }
77
+
78
+ function toCloudDevinUsage(usage: OcxUsage): DevinUsageEvent {
79
+ return {
80
+ kind: "usage",
81
+ promptTokens: usage.inputTokens,
82
+ completionTokens: usage.outputTokens,
83
+ totalTokens: usage.totalTokens,
84
+ cachedInputTokens: usage.cachedInputTokens,
85
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
86
+ reasoningTokens: usage.reasoningOutputTokens,
87
+ };
88
+ }
89
+
90
+ /** The retry's cumulative usage plus the refused attempt's final counts. */
91
+ function addDevinUsage(event: DevinUsageEvent, prior: DevinUsageEvent): DevinUsageEvent {
92
+ const sum = (a?: number, b?: number) => (a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0));
93
+ const out: DevinUsageEvent = { ...event };
94
+ for (const key of ["promptTokens", "completionTokens", "totalTokens", "cachedInputTokens", "cacheCreationInputTokens", "reasoningTokens"] as const) {
95
+ const value = sum(event[key], prior[key]);
96
+ if (value !== undefined) out[key] = value;
97
+ }
98
+ return out;
99
+ }
100
+
57
101
  /** Map a cloud-direct failure onto the structured fields the error event carries. */
58
102
  export function devinErrorClassification(error: unknown): { status?: number; errorType?: string; retryable?: boolean } {
59
103
  const status = error instanceof CloudChatError ? error.status : undefined;
@@ -131,7 +175,9 @@ const SWE2_EFFORT: Record<string, "medium" | "high" | "max"> = {
131
175
 
132
176
  /**
133
177
  * Resolve an explicit effort onto a SWE-2 lane, or undefined when this is not a
134
- * SWE-2 id or the caller named no usable effort. Undefined leaves every existing
178
+ * SWE-2 id or the caller named no usable effort. Reached only when the catalog
179
+ * is unavailable or lacks family metadata; otherwise the catalog's SWE-2 family
180
+ * rows decide, with the same rounding. Undefined leaves every existing
135
181
  * path untouched, which is what keeps other model families on suffix precedence.
136
182
  */
137
183
  function resolveSwe2Variant(modelId: string, reasoningEffort?: string): string | undefined {
@@ -140,17 +186,92 @@ function resolveSwe2Variant(modelId: string, reasoningEffort?: string): string |
140
186
  return mapped ? `swe-2-${mapped}` : undefined;
141
187
  }
142
188
 
189
+ const VARIANT_EFFORT_TOKENS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
190
+ const EFFORT_ALIASES: Record<string, string> = { off: "none", ultra: "max" };
191
+
143
192
  /**
144
- * Resolve the wire model UID using the live catalog as the source of truth.
145
- * Cognition's catalog lists most models with an effort suffix
146
- * (e.g. `gpt-5-6-sol-high`); the base id alone is not accepted for those.
193
+ * Read hyphen-separated variant tokens as a request. Any token outside the
194
+ * vocabulary voids the whole value, so an unknown effort changes nothing
195
+ * rather than half-applying. `priority` is the uid spelling of Fast Mode on
196
+ * the GPT rows; a caller never sends it as effort (see CALLER_EFFORT_VALUES).
197
+ */
198
+ function variantRequestOf(tokens: string[], allowPriority: boolean): DevinVariantRequest | undefined {
199
+ const request: DevinVariantRequest = {};
200
+ for (const raw of tokens) {
201
+ const token = EFFORT_ALIASES[raw] ?? raw;
202
+ if (VARIANT_EFFORT_TOKENS.has(token)) request.effort = token;
203
+ else if (token === "fast" || (allowPriority && token === "priority")) request.fast = true;
204
+ else if (token === "1m") request.longContext = true;
205
+ else return undefined;
206
+ }
207
+ return request;
208
+ }
209
+
210
+ function callerVariantRequest(reasoningEffort: string | undefined): DevinVariantRequest | undefined {
211
+ const value = reasoningEffort?.toLowerCase();
212
+ if (!value) return undefined;
213
+ const aliased = EFFORT_ALIASES[value] ?? value;
214
+ if (!CALLER_EFFORT_VALUES.has(aliased) && aliased !== "minimal") return undefined;
215
+ return variantRequestOf(aliased.split("-"), false);
216
+ }
217
+
218
+ /**
219
+ * Resolve through the catalog's family metadata (ClientModelConfig #23/#30/#31).
220
+ * Returns undefined when the id belongs to no family, which leaves the
221
+ * suffix-based path for legacy rows and catalogs without that metadata.
147
222
  *
148
- * If the catalog is available: use the exact UID when it exists, otherwise
149
- * append the reasoning effort (or `medium` default) and pick a variant the
150
- * account actually has.
223
+ * The family id wins over an identical row uid on purpose: bare `swe-1-7` is
224
+ * the Max row while the family default is `swe-1-7-medium`, and `glm-5-2` is
225
+ * both the family id and its default row. A caller naming the family means the
226
+ * family, so no effort selects the default member and an effort moves only the
227
+ * effort axis. A caller naming a member row keeps that row's other axes.
228
+ */
229
+ function resolveFamilyUid(catalog: CacheEntry, modelId: string, reasoningEffort?: string): string | undefined {
230
+ const families = devinFamiliesOf(catalog);
231
+ const caller = callerVariantRequest(reasoningEffort);
232
+ let members = families.get(modelId);
233
+ let anchor: ModelCatalogEntry | undefined;
234
+ let fromId: DevinVariantRequest = {};
235
+ if (!members) {
236
+ const row = catalog.byUid.get(modelId);
237
+ if (row?.familyUid) {
238
+ members = families.get(devinFamilyBaseId(row.familyUid));
239
+ if (members) anchor = row;
240
+ }
241
+ }
242
+ if (!members) {
243
+ // A suffixed spelling the catalog does not list (`swe-1-7-high`) still
244
+ // names a family; its suffix is the request.
245
+ const base = collapseDevinModelUid(modelId);
246
+ const suffix = base !== modelId ? variantRequestOf(modelId.slice(base.length + 1).split("-"), true) : undefined;
247
+ members = suffix ? families.get(base) : undefined;
248
+ if (suffix) fromId = suffix;
249
+ }
250
+ if (!members) return undefined;
251
+ if (anchor && !caller) return anchor.modelUid;
252
+ const request: DevinVariantRequest = {
253
+ ...fromId,
254
+ ...(caller?.effort ? { effort: caller.effort } : {}),
255
+ ...(caller?.fast || fromId.fast ? { fast: true } : {}),
256
+ ...(caller?.longContext || fromId.longContext ? { longContext: true } : {}),
257
+ };
258
+ // A disabled row the caller named keeps its uid when the request still lands
259
+ // on it (`swe-2-max` asked for at `max`), so the chat preflight reports that
260
+ // row's tier refusal instead of quietly serving a different tier. A request
261
+ // for a different variant still passes over disabled rows.
262
+ if (anchor?.disabled && selectDevinFamilyMember(members, request, anchor, { includeDisabled: true }) === anchor) {
263
+ return anchor.modelUid;
264
+ }
265
+ return selectDevinFamilyMember(members, request, anchor)?.modelUid;
266
+ }
267
+
268
+ /**
269
+ * Resolve the wire model UID using the live catalog as the source of truth.
151
270
  *
152
- * If the catalog is unavailable (degraded mode): append the effort suffix
153
- * for any base id that doesn't already carry one, mirroring the catalog shape.
271
+ * With family metadata in the catalog, resolution picks a family member by
272
+ * axis (see resolveFamilyUid). Without it — legacy rows, an older catalog, or
273
+ * no catalog at all — the id is resolved by suffix: an exact or suffixed UID is
274
+ * kept, otherwise the reasoning effort (or `medium`) is appended.
154
275
  */
155
276
  async function resolveWireModelUid(
156
277
  rawModelId: string,
@@ -160,28 +281,33 @@ async function resolveWireModelUid(
160
281
  catalog?: CacheEntry | null,
161
282
  ): Promise<string> {
162
283
  const modelId = normalizeDevinModelId(rawModelId);
284
+ // Callers that already read the catalog this turn pass it in; an explicit
285
+ // null records a failed lookup and must not trigger a same-turn retry —
286
+ // failures are not cached, so re-reading would only pay another timeout.
287
+ const entry = catalog !== undefined ? catalog : await getCachedCatalog(apiKey, host);
288
+ if (entry) {
289
+ const fromFamily = resolveFamilyUid(entry, modelId, reasoningEffort);
290
+ if (fromFamily) return fromFamily;
291
+ }
163
292
  // Explicit effort wins over a suffix the picker already baked into the id, so
164
293
  // `swe-2-high` asked for at `medium` becomes `swe-2-medium` instead of ignoring
165
294
  // the caller. Runs before the shortcut below, which would otherwise return early.
166
295
  const swe2 = resolveSwe2Variant(modelId, reasoningEffort);
167
296
  if (swe2) return swe2;
168
297
  if (hasEffortSuffix(modelId)) return modelId;
169
- // Callers that already read the catalog this turn pass it in; an explicit
170
- // null records a failed lookup and must not trigger a same-turn retry —
171
- // failures are not cached, so re-reading would only pay another timeout.
172
- const entry = catalog !== undefined ? catalog : await getCachedCatalog(apiKey, host);
298
+ const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
173
299
  if (entry) {
174
300
  if (entry.byUid.has(modelId)) return modelId;
175
- const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
176
301
  const suffixed = `${modelId}-${effort}`;
177
302
  if (entry.byUid.has(suffixed)) return suffixed;
178
- // Fall back to any enabled variant of this base model.
179
- for (const uid of entry.byUid.keys()) {
180
- if (uid.startsWith(modelId + "-") && !entry.byUid.get(uid)?.disabled) return uid;
303
+ // Any enabled variant of this exact base. The base must match after
304
+ // collapsing, not as a string prefix: `claude-opus-5-` prefixes
305
+ // `claude-opus-5-5-low` and `gpt-5-4-` prefixes `gpt-5-4-mini-low`.
306
+ for (const [uid, row] of entry.byUid) {
307
+ if (!row.disabled && uid !== modelId && collapseDevinModelUid(uid) === modelId) return uid;
181
308
  }
182
309
  }
183
310
  // Degraded mode: append the default effort suffix.
184
- const effort = reasoningEffort && CALLER_EFFORT_VALUES.has(reasoningEffort) ? reasoningEffort : "medium";
185
311
  return `${modelId}-${effort}`;
186
312
  }
187
313
 
@@ -198,7 +324,9 @@ const positiveTokenCount = (value: unknown): number | undefined =>
198
324
  /**
199
325
  * Read a per-model token count for the exact UID selected for this turn.
200
326
  *
201
- * Tries the selected UID and then its collapsed base id, preferring the
327
+ * Tries the selected UID, then its catalog family id (the picker id, e.g.
328
+ * `claude-sonnet-4-6` for `claude-sonnet-4-6-thinking`), then its collapsed
329
+ * base id, preferring the
202
330
  * canonical spelling and accepting dotted or case-folded saved hints — the same
203
331
  * normalization the inference request applies to the model id. Where several
204
332
  * spellings match one id, the smallest wins: a ceiling stated twice is
@@ -207,9 +335,10 @@ const positiveTokenCount = (value: unknown): number | undefined =>
207
335
  function devinModelTokenHint(
208
336
  record: Record<string, number> | undefined,
209
337
  modelUid: string,
338
+ familyBase?: string,
210
339
  ): number | undefined {
211
340
  if (!record) return undefined;
212
- for (const id of [modelUid, collapseDevinModelUid(modelUid)]) {
341
+ for (const id of [modelUid, ...(familyBase ? [familyBase] : []), collapseDevinModelUid(modelUid)]) {
213
342
  const exact = Object.hasOwn(record, id) ? positiveTokenCount(record[id]) : undefined;
214
343
  if (exact !== undefined) return exact;
215
344
  const matches = Object.entries(record)
@@ -221,26 +350,6 @@ function devinModelTokenHint(
221
350
  return undefined;
222
351
  }
223
352
 
224
- /**
225
- * Resolve the INPUT ceiling for the exact UID selected for this turn. Catalog
226
- * ClientModelConfig #18 and CompletionConfiguration #3 both carry input tokens;
227
- * the independent output cap is not subtracted here. Smaller operator hints
228
- * cap live evidence, never enlarge it. No evidence leaves the encoder's 128k
229
- * fallback intact; an unrelated or opt-in long-context variant is not evidence.
230
- */
231
- function resolveDevinMaxInputTokens(
232
- provider: OcxProviderConfig,
233
- modelUid: string,
234
- liveWindow?: number,
235
- ): number | undefined {
236
- const contextHint = devinModelTokenHint(provider.modelContextWindows, modelUid)
237
- ?? positiveTokenCount(provider.contextWindow);
238
- const inputHint = devinModelTokenHint(provider.modelMaxInputTokens, modelUid);
239
- const ceilings = [positiveTokenCount(liveWindow), contextHint, inputHint]
240
- .filter((value): value is number => value !== undefined);
241
- return ceilings.length > 0 ? Math.min(...ceilings) : undefined;
242
- }
243
-
244
353
  /**
245
354
  * Resolve the OUTPUT ceiling for this turn, highest authority first:
246
355
  *
@@ -248,16 +357,17 @@ function resolveDevinMaxInputTokens(
248
357
  * explicit cap is a request, so a small one is never widened into a
249
358
  * configured larger one;
250
359
  * 2. the configured per-model cap (`modelMaxOutputTokens`), read through the
251
- * same UID-aware hint lookup the input ceiling uses;
360
+ * through the UID-aware hint lookup above;
252
361
  * 3. the provider-wide `defaultMaxOutputTokens`;
253
- * 4. undefined, which leaves the cloud-direct encoder's own 8192 fallback in
254
- * place for a provider that configured nothing.
362
+ * 4. the catalog's own ceiling for the selected UID (ModelInfo #13) — without
363
+ * it every uncapped turn stopped at the encoder's 8192, far below the
364
+ * 128k most rows advertise;
365
+ * 5. undefined, which leaves the cloud-direct encoder's 8192 fallback in
366
+ * place when there is no catalog and nothing configured.
255
367
  *
256
- * This is NOT the history ceiling, and the two must not collapse into one
257
- * number. CompletionConfiguration #2 is the output cap and #3 is the context
258
- * window, so feeding a context window into this resolver would ask Cognition to
259
- * generate a whole window's worth of output. Nothing here reads
260
- * `contextWindow` or `modelContextWindows` for that reason.
368
+ * A context window is not an output cap: feeding one into CompletionConfiguration
369
+ * #2 would ask Cognition to generate a whole window's worth of output. Nothing
370
+ * here reads `contextWindow` or `modelContextWindows` for that reason.
261
371
  *
262
372
  * Step 1 keeps the caller's raw value rather than `positiveTokenCount`: the
263
373
  * inbound parser owns what a caller may send, and re-filtering here would
@@ -268,16 +378,37 @@ function resolveDevinMaxOutputTokens(
268
378
  provider: OcxProviderConfig,
269
379
  modelUid: string,
270
380
  requested: number | undefined,
381
+ catalogRow?: Pick<ModelCatalogEntry, "maxOutputTokens" | "familyUid">,
271
382
  ): number | undefined {
272
383
  if (typeof requested === "number") return requested;
273
- return devinModelTokenHint(provider.modelMaxOutputTokens, modelUid)
274
- ?? positiveTokenCount(provider.defaultMaxOutputTokens);
384
+ const familyBase = catalogRow?.familyUid ? devinFamilyBaseId(catalogRow.familyUid) : undefined;
385
+ return devinModelTokenHint(provider.modelMaxOutputTokens, modelUid, familyBase)
386
+ ?? positiveTokenCount(provider.defaultMaxOutputTokens)
387
+ ?? positiveTokenCount(catalogRow?.maxOutputTokens);
275
388
  }
276
389
 
277
- /** Pure test seams; runtime uses the same resolvers immediately before dispatch. */
278
- export const resolveDevinMaxInputTokensForTests = resolveDevinMaxInputTokens;
390
+ /** Pure test seam; runtime uses the same resolver immediately before dispatch. */
279
391
  export const resolveDevinMaxOutputTokensForTests = resolveDevinMaxOutputTokens;
280
392
 
393
+ /** The classifier reads the selected UID's catalog input window, capped by configured limits. */
394
+ function resolveDevinContextWindow(
395
+ provider: OcxProviderConfig,
396
+ modelUid: string,
397
+ catalogRow?: Pick<ModelCatalogEntry, "contextWindow" | "familyUid">,
398
+ ): number | undefined {
399
+ const familyBase = catalogRow?.familyUid ? devinFamilyBaseId(catalogRow.familyUid) : undefined;
400
+ const limits = [
401
+ positiveTokenCount(catalogRow?.contextWindow),
402
+ devinModelTokenHint(provider.modelContextWindows, modelUid, familyBase),
403
+ positiveTokenCount(provider.contextWindow),
404
+ devinModelTokenHint(provider.modelMaxInputTokens, modelUid, familyBase),
405
+ ].filter((value): value is number => value !== undefined);
406
+ return limits.length > 0 ? Math.min(...limits) : undefined;
407
+ }
408
+
409
+ /** Pure test seam for configured caps and selected-row lookup. */
410
+ export const resolveDevinContextWindowForTests = resolveDevinContextWindow;
411
+
281
412
  export class DevinMissingCredentialError extends Error {
282
413
  constructor() {
283
414
  super("Devin live transport requires a Devin API key. Run ocx login devin to sign in with your Cognition/Devin account.");
@@ -364,45 +495,16 @@ function assistantText(message: OcxAssistantMessage): string {
364
495
  // Thinking stays out of the replayed TEXT: folding chain-of-thought into
365
496
  // assistant text sends it back as visible prior output, which the model
366
497
  // then treats as something it said to the user. It is replayed in its own
367
- // field instead — see assistantThinking below.
498
+ // field instead — see devinAssistantReasoning.
368
499
  .map((part) => (part.type === "text" ? part.text : ""))
369
500
  .filter(Boolean)
370
501
  .join("\n");
371
502
  }
372
503
 
373
- /**
374
- * The assistant turn's own reasoning, for replay in ChatMessagePrompt #11.
375
- *
376
- * This adapter previously asserted that Cognition has no reasoning-replay
377
- * field and dropped the thinking outright, so a reasoning model restarted its
378
- * chain on every turn of a tool loop. The field exists: two independent
379
- * clients of the same service write #11 thinking with #12 signature and #18
380
- * signature_type on the assistant prompt.
381
- *
382
- * Field #12 attests the exact text at #11, and the wire has room for one pair.
383
- * Every block that carries text is replayed, so the chain stays intact; the
384
- * signature rides along only when the text being replayed IS the text it
385
- * attests, which is exactly the single-block case. Several independently signed
386
- * blocks send an unsigned prompt rather than pairing one block's attestation
387
- * with another block's words. A signature-only block attests encrypted thinking
388
- * that is not being replayed at all, so it is not one of these blocks and
389
- * cannot contribute the pair.
390
- */
391
- function assistantThinking(
392
- message: OcxAssistantMessage,
393
- ): { thinking?: string; signature?: string } {
394
- const blocks = message.content.filter(
395
- (part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
396
- ).filter(part => Boolean(part.thinking));
397
- if (blocks.length === 0) return {};
398
- const signature = blocks.length === 1 ? blocks[0]!.signature : undefined;
399
- return {
400
- thinking: blocks.map(part => part.thinking).join("\n"),
401
- ...(isProviderIssuedThinkingSignature(signature) ? { signature } : {}),
402
- };
403
- }
404
-
405
- export function mapOcxMessagesToDevin(parsed: OcxParsedRequest): ChatHistoryItem[] {
504
+ export function mapOcxMessagesToDevin(
505
+ parsed: OcxParsedRequest,
506
+ options: { withholdAnthropicSignatures?: boolean } = {},
507
+ ): ChatHistoryItem[] {
406
508
  const items: ChatHistoryItem[] = [];
407
509
  // Cognition is not an OpenAI host, and this adapter does advertise a real
408
510
  // client tool catalog (proto #10 via `mapOcxToolsToDevin`), so the same
@@ -421,13 +523,17 @@ export function mapOcxMessagesToDevin(parsed: OcxParsedRequest): ChatHistoryItem
421
523
  if (system) items.push({ role: "system", content: system });
422
524
 
423
525
  for (const message of parsed.context.messages) {
424
- const mapped = mapOneMessage(message);
526
+ const mapped = mapOneMessage(message, parsed.modelId, options);
425
527
  if (mapped) items.push(mapped);
426
528
  }
427
529
  return items;
428
530
  }
429
531
 
430
- function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
532
+ function mapOneMessage(
533
+ message: OcxMessage,
534
+ modelId: string,
535
+ options: { withholdAnthropicSignatures?: boolean },
536
+ ): ChatHistoryItem | undefined {
431
537
  if (message.role === "user" || message.role === "developer") {
432
538
  const content = mapOcxContentToWire(message.content);
433
539
  // An image with no caption text is a complete user message on its own.
@@ -439,10 +545,10 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
439
545
  if (message.role === "assistant") {
440
546
  const toolCalls = assistantToolCalls(message);
441
547
  const text = assistantText(message);
442
- const reasoning = assistantThinking(message);
548
+ const reasoning = devinAssistantReasoning(message, modelId, options.withholdAnthropicSignatures === true);
443
549
  // A turn that produced only reasoning is still worth replaying: dropping it
444
550
  // is what makes the next turn re-derive the same chain.
445
- if (!text && toolCalls.length === 0 && !reasoning.thinking) return undefined;
551
+ if (!text && toolCalls.length === 0 && !reasoning.thinking && !reasoning.signature) return undefined;
446
552
  return {
447
553
  role: "assistant",
448
554
  content: text || "",
@@ -451,6 +557,10 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
451
557
  };
452
558
  }
453
559
  if (message.role === "toolResult") {
560
+ // #9 alone is not enough: live, with a neutral "hello world" result flagged
561
+ // as an error, only gemini-3-8-flash reported a failure; swe-1-6,
562
+ // gpt-6-sol-low and gpt-5-6-luna-low read it as success. So the flag rides
563
+ // with the in-band marker rather than replacing it.
454
564
  const wireContent = mapOcxContentToWire(message.content);
455
565
  const toolContent = message.isError
456
566
  ? (typeof wireContent === "string"
@@ -461,6 +571,7 @@ function mapOneMessage(message: OcxMessage): ChatHistoryItem | undefined {
461
571
  role: "tool",
462
572
  content: toolContent,
463
573
  tool_call_id: message.toolCallId,
574
+ ...(message.isError ? { is_error: true } : {}),
464
575
  };
465
576
  }
466
577
  return undefined;
@@ -613,8 +724,8 @@ export function createDevinAdapter(
613
724
  // entry: an EU or FedStart account that used provider.baseUrl would send
614
725
  // every RPC to the US server it is not provisioned on.
615
726
  const host = resolveDevinApiServer(provider.baseUrl, credentialProviderId, apiKey);
616
- // One catalog read per turn serves model-UID resolution, the input
617
- // ceiling, and the chat pre-flight inside streamChatEvents. Failures are
727
+ // One catalog read per turn serves model-UID resolution, the output
728
+ // cap, and the chat pre-flight inside streamChatEvents. Failures are
618
729
  // not cached, so a second read would only pay another fetch timeout on
619
730
  // an otherwise valid turn.
620
731
  const catalog = await getCachedCatalog(apiKey, host, incoming.abortSignal);
@@ -627,6 +738,11 @@ export function createDevinAdapter(
627
738
  let openToolId: string | undefined;
628
739
  let usage: OcxUsage | undefined;
629
740
  let stopReason: string | undefined;
741
+ // Kept outside the try so the catch can tell an oversized history from a bad request.
742
+ let producedOutput = false;
743
+ let contextWindow: number | undefined;
744
+ let messages: ChatHistoryItem[] = [];
745
+ let tools: ToolDef[] | undefined;
630
746
 
631
747
  const closeOpenTool = () => {
632
748
  if (!openToolId) return;
@@ -636,11 +752,11 @@ export function createDevinAdapter(
636
752
 
637
753
  try {
638
754
  // Read the selected UID's catalog row, not the picker's collapsed base.
639
- const maxInputTokens = resolveDevinMaxInputTokens(
640
- provider, modelUid, catalog?.byUid.get(modelUid)?.contextWindow,
641
- );
755
+ contextWindow = resolveDevinContextWindow(provider, modelUid, catalog?.byUid.get(modelUid));
756
+ messages = mapOcxMessagesToDevin(parsed);
757
+ tools = mapOcxToolsToDevin(parsed.context.tools);
642
758
  const maxOutputTokens = resolveDevinMaxOutputTokens(
643
- provider, modelUid, parsed.options.maxOutputTokens,
759
+ provider, modelUid, parsed.options.maxOutputTokens, catalog?.byUid.get(modelUid),
644
760
  );
645
761
  // A combo child has not committed an outer response yet. Holding its preflight through
646
762
  // a reset wait would also hold the next-target fallback with no client keepalive.
@@ -648,18 +764,23 @@ export function createDevinAdapter(
648
764
  // An admitted HTTP turn owns globally shared capacity until this call
649
765
  // emits. Without an explicit wait allowance, preserve the typed reset
650
766
  // delay in generated diagnostic wording and return immediately.
651
- for await (const event of streamChatEventsWithResetRetry({
767
+ const signedMessages = mapOcxMessagesToDevin(parsed);
768
+ // A Claude signature is replayed because it is what carries the reasoning into this
769
+ // turn, but Cognition streams Claude's thinking as a summary the signature does not
770
+ // cover, and some replays are refused with invalid_argument before any output. That
771
+ // refusal is retried once with the Anthropic signatures withheld and the text kept.
772
+ const unsignedMessages = hasAnthropicSignature(signedMessages, parsed.modelId)
773
+ ? mapOcxMessagesToDevin(parsed, { withholdAnthropicSignatures: true })
774
+ : undefined;
775
+ const request = (messages: ChatHistoryItem[]) => streamChatEventsWithResetRetry({
652
776
  apiKey,
653
777
  apiServerUrl: host,
654
778
  modelUid,
655
779
  catalog,
656
- messages: mapOcxMessagesToDevin(parsed),
657
- tools: mapOcxToolsToDevin(parsed.context.tools),
780
+ messages,
781
+ tools,
658
782
  cascadeId,
659
- // Input and output ceilings are separate wire fields. Omitting the
660
- // input hint used to force every model through the 128k default.
661
783
  completionOpts: {
662
- ...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
663
784
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
664
785
  ...(typeof parsed.options.temperature === "number" ? { temperature: parsed.options.temperature } : {}),
665
786
  ...(typeof parsed.options.topP === "number" ? { topP: parsed.options.topP } : {}),
@@ -675,7 +796,80 @@ export function createDevinAdapter(
675
796
  onPhysicalSend: incoming.onPhysicalSend,
676
797
  onRecoveryWithheld: incoming.onRecoveryWithheld,
677
798
  },
678
- })) {
799
+ });
800
+ async function* withSignatureFallback() {
801
+ if (!unsignedMessages) {
802
+ yield* request(signedMessages);
803
+ return;
804
+ }
805
+ // Events from the signed attempt are held until its outcome is known: a refusal
806
+ // after reasoning would otherwise leave the client with the refused attempt's
807
+ // reasoning and signature, and the next turn would replay that signature against
808
+ // the retry's thinking.
809
+ const held: CloudChatEvent[] = [];
810
+ // Usage is still real: the refused attempt was processed, so its final counts are
811
+ // added to every usage frame of the retry (frames are cumulative per request).
812
+ let refusedUsage: OcxUsage | undefined;
813
+ let visible = false;
814
+ let heldPayloadBytes = 0;
815
+ // The iterator may pause before a trailer. A timer feeds the bridge during that
816
+ // pause without starting another upstream read or marking replay unsafe.
817
+ const heartbeatTimer = setInterval(() => {
818
+ if (!visible) emit({ type: "heartbeat" });
819
+ }, HELD_REASONING_HEARTBEAT_MS);
820
+ try {
821
+ for await (const event of request(signedMessages)) {
822
+ // Only visible output makes a retry unsafe. Live, the refusal often lands after the
823
+ // model has streamed its reasoning, its signature and a finish frame, and nothing else.
824
+ if (!visible && (event.kind === "text" || event.kind === "tool_call_start" || event.kind === "tool_call_args")) {
825
+ visible = true;
826
+ clearInterval(heartbeatTimer);
827
+ yield* held.splice(0);
828
+ }
829
+ if (visible) {
830
+ yield event;
831
+ continue;
832
+ }
833
+ held.push(event);
834
+ if (event.kind === "usage") {
835
+ const next = toOcxDevinUsage(event);
836
+ refusedUsage = refusedUsage ? mergeDevinUsage(refusedUsage, next) : next;
837
+ }
838
+ if (event.kind === "reasoning") heldPayloadBytes += event.text.length * 2;
839
+ if (event.kind === "reasoning_signature") heldPayloadBytes += event.signature.length * 2;
840
+ if (held.length > HELD_REASONING_MAX_EVENTS || heldPayloadBytes > HELD_REASONING_MAX_PAYLOAD_BYTES) {
841
+ visible = true;
842
+ clearInterval(heartbeatTimer);
843
+ yield* held.splice(0);
844
+ }
845
+ }
846
+ } catch (error) {
847
+ clearInterval(heartbeatTimer);
848
+ if (visible || !(error instanceof CloudChatError && error.code === "invalid_argument")) {
849
+ yield* held.splice(0);
850
+ throw error;
851
+ }
852
+ // Emitted first so the counts survive a retry that reports no usage or fails early.
853
+ const cumulativeRefusedUsage = refusedUsage ? toCloudDevinUsage(refusedUsage) : undefined;
854
+ if (cumulativeRefusedUsage) yield cumulativeRefusedUsage;
855
+ try {
856
+ for await (const event of request(unsignedMessages)) {
857
+ yield event.kind === "usage" && cumulativeRefusedUsage ? addDevinUsage(event, cumulativeRefusedUsage) : event;
858
+ }
859
+ } catch (retryError) {
860
+ if (retryError instanceof SendBudgetExhaustedError) {
861
+ incoming.onRecoveryWithheld?.({ reason: "retry-send-budget" });
862
+ throw error;
863
+ }
864
+ throw retryError;
865
+ }
866
+ return;
867
+ } finally {
868
+ clearInterval(heartbeatTimer);
869
+ }
870
+ yield* held.splice(0);
871
+ }
872
+ for await (const event of withSignatureFallback()) {
679
873
  if (incoming.abortSignal?.aborted) {
680
874
  // Emitting nothing here left the bridge to synthesize adapter_eof.
681
875
  // Say what happened instead, the way the other runTurn-only adapter
@@ -684,6 +878,7 @@ export function createDevinAdapter(
684
878
  emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
685
879
  return;
686
880
  }
881
+ if (event.kind === "text" || event.kind === "reasoning" || event.kind === "tool_call_start") producedOutput = true;
687
882
  if (event.kind === "text") {
688
883
  closeOpenTool();
689
884
  if (event.text) emit({ type: "text_delta", text: event.text });
@@ -696,7 +891,7 @@ export function createDevinAdapter(
696
891
  if (event.kind === "reasoning_signature") {
697
892
  // Carried back out so the next turn can replay it in the prompt's
698
893
  // signature field; an unsigned replay is what the service ignores.
699
- emit({ type: "thinking_signature", signature: event.signature });
894
+ emit({ type: "thinking_signature", signature: encodeDevinSignature(event.signature, event.signatureType) });
700
895
  continue;
701
896
  }
702
897
  if (event.kind === "tool_call_start") {
@@ -724,15 +919,7 @@ export function createDevinAdapter(
724
919
  continue;
725
920
  }
726
921
  if (event.kind === "usage") {
727
- const total = event.totalTokens ?? ((event.promptTokens ?? 0) + (event.completionTokens ?? 0));
728
- const next: OcxUsage = {
729
- inputTokens: event.promptTokens ?? 0,
730
- outputTokens: event.completionTokens ?? 0,
731
- ...(total > 0 ? { totalTokens: total } : {}),
732
- ...(event.cachedInputTokens !== undefined ? { cachedInputTokens: event.cachedInputTokens } : {}),
733
- ...(event.cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens: event.cacheCreationInputTokens } : {}),
734
- ...(event.reasoningTokens !== undefined ? { reasoningOutputTokens: event.reasoningTokens } : {}),
735
- };
922
+ const next = toOcxDevinUsage(event);
736
923
  // Merge rather than replace. A turn can carry more than one usage
737
924
  // frame, and the counters are cumulative, so a later partial frame
738
925
  // that omits a field used to zero a count the earlier frame had
@@ -756,6 +943,12 @@ export function createDevinAdapter(
756
943
  // The Responses boundary already maps this local refusal to its structured 429 code.
757
944
  // Converting it to an adapter event would make it an ordinary untyped upstream error.
758
945
  if (error instanceof SendBudgetExhaustedError) throw error;
946
+ if (error instanceof CloudChatError && isDevinHistoryOverflow({
947
+ code: error.code, producedOutput, contextWindow, messages, tools,
948
+ })) {
949
+ emit({ ...devinContextOverflowEvent(), ...(usage ? { usage } : {}) });
950
+ return;
951
+ }
759
952
  const message = error instanceof CloudChatError
760
953
  ? ("Devin cloud error" + (error.code ? " " + error.code : "") + ": " + error.message)
761
954
  : error instanceof Error ? error.message : String(error);