@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
@@ -2,12 +2,12 @@
2
2
  * Live Devin / Cognition model discovery via GetCascadeModelConfigs.
3
3
  *
4
4
  * The live catalog is the source of truth for the model roster. The endpoint
5
- * returns effort-suffixed variants (e.g. `gpt-5-6-sol-high`); we collapse those
6
- * to base ids so the picker stays clean and the adapter appends the effort
7
- * suffix at request time. `DEVIN_STATIC_MODELS` is only a degraded-mode
5
+ * returns one row per variant (e.g. `gpt-5-6-sol-high`), grouped into families;
6
+ * the picker shows one id per family and the adapter picks the family member
7
+ * for the requested effort at request time. `DEVIN_STATIC_MODELS` is only a degraded-mode
8
8
  * fallback for when there is no API key or discovery fails.
9
9
  */
10
- import { getCachedCatalog, type ModelCatalogEntry } from "./cloud-direct";
10
+ import { getCachedCatalog, type CacheEntry, type ModelCatalogEntry } from "./cloud-direct";
11
11
 
12
12
  const DEFAULT_HOST = "https://server.codeium.com";
13
13
 
@@ -66,6 +66,8 @@ export const DEVIN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
66
66
  "claude-opus-5-5": 1_000_000,
67
67
  "claude-opus-5": 1_000_000,
68
68
  "claude-fable-5-1": 1_000_000,
69
+ // 260929 preemptive: Claude Sonnet 5.5 (1M per Anthropic) seeded before Devin's live catalog lists it.
70
+ "claude-sonnet-5-5": 1_000_000,
69
71
  "claude-sonnet-5": 1_000_000,
70
72
  "glm-5-2": 200_000,
71
73
  "glm-5-3": 1_048_576,
@@ -103,7 +105,7 @@ export function collapseDevinModelUid(uid: string): string {
103
105
  * Offering them on a reasoning control would name a setting that does something
104
106
  * else, so the collapse keeps stripping them while the ladder ignores them.
105
107
  */
106
- const REASONING_RUNG_TOKENS = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
108
+ const REASONING_RUNG_TOKENS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
107
109
 
108
110
  /**
109
111
  * The reasoning rungs a catalog UID carries, in ladder order.
@@ -124,11 +126,154 @@ export function devinReasoningRungsOf(uid: string): string[] {
124
126
  }
125
127
 
126
128
  /** Ladder order for display, matching the Codex rung order. */
127
- const RUNG_ORDER = ["none", "low", "medium", "high", "xhigh", "max"];
129
+ const RUNG_ORDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
128
130
  export function sortDevinRungs(rungs: Iterable<string>): string[] {
129
131
  return [...new Set(rungs)].sort((a, b) => RUNG_ORDER.indexOf(a) - RUNG_ORDER.indexOf(b));
130
132
  }
131
133
 
134
+ /**
135
+ * Effort rungs a catalog family axis can name, in ladder order.
136
+ * The catalog spells `Minimal` (Gemini Flash) and `No Thinking` (GLM), which
137
+ * resolution and the picker place alongside the usual Codex rungs.
138
+ */
139
+ const FAMILY_EFFORT_LADDER = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
140
+ const EFFORT_AXES = ["Effort", "Reasoning Effort"];
141
+ const FAST_AXIS = "Fast Mode";
142
+ const LONG_CONTEXT_AXIS = "1M Context";
143
+ const THINKING_AXIS = "Thinking";
144
+
145
+ /** The picker id of a catalog family: its uid with dotted versions hyphenated, as the row uids spell it. */
146
+ export function devinFamilyBaseId(familyUid: string): string {
147
+ return familyUid.replace(/\./g, "-");
148
+ }
149
+
150
+ function isEffortAxis(axis: string): boolean {
151
+ return EFFORT_AXES.includes(axis);
152
+ }
153
+
154
+ /** The effort rung a family member sits on, read from its axis value name rather than its uid. */
155
+ export function devinFamilyEffortOf(entry: ModelCatalogEntry): string | undefined {
156
+ const axes = entry.familyAxes;
157
+ if (!axes) return undefined;
158
+ for (const axis of EFFORT_AXES) {
159
+ const name = axes[axis]?.name?.toLowerCase();
160
+ if (!name) continue;
161
+ const rung = name === "no thinking" ? "none" : name;
162
+ if (FAMILY_EFFORT_LADDER.includes(rung)) return rung;
163
+ }
164
+ return undefined;
165
+ }
166
+
167
+ const familyCache = new WeakMap<CacheEntry, Map<string, ModelCatalogEntry[]>>();
168
+
169
+ /**
170
+ * Group catalog rows by family, keyed by the family's picker id. Only rows
171
+ * that carry family metadata take part; legacy `MODEL_*` rows are internal
172
+ * enum names and never form a picker family.
173
+ */
174
+ export function devinFamiliesOf(catalog: CacheEntry): Map<string, ModelCatalogEntry[]> {
175
+ let families = familyCache.get(catalog);
176
+ if (families) return families;
177
+ families = new Map();
178
+ for (const entry of catalog.byUid.values()) {
179
+ if (!entry.familyUid || !entry.familyAxes || entry.modelUid.startsWith("MODEL_")) continue;
180
+ const base = devinFamilyBaseId(entry.familyUid);
181
+ const members = families.get(base);
182
+ if (members) members.push(entry);
183
+ else families.set(base, [entry]);
184
+ }
185
+ familyCache.set(catalog, families);
186
+ return families;
187
+ }
188
+
189
+ /** What a caller asked for, independent of how the catalog spells it. */
190
+ export interface DevinVariantRequest {
191
+ effort?: string;
192
+ fast?: boolean;
193
+ longContext?: boolean;
194
+ }
195
+
196
+ function lexicallyLess(a: number[], b: number[]): boolean {
197
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i]! < b[i]!;
198
+ return false;
199
+ }
200
+
201
+ function effortIndex(rung: string | undefined): number {
202
+ return rung === undefined ? -1 : FAMILY_EFFORT_LADDER.indexOf(rung);
203
+ }
204
+
205
+ /**
206
+ * The member closest to `targets` on the non-effort axes, then the lowest rung
207
+ * at or above `effort`, falling back to the highest rung below it. A missing
208
+ * rung never quietly turns reasoning down: `high` on SWE-1.7 (Medium, Max)
209
+ * selects Max, `xhigh` on SWE-2 selects Max, and `medium` on Kimi K3 (Low,
210
+ * High, Max) selects High.
211
+ *
212
+ * Not lowering a requested effort outranks the non-effort axes: in a family
213
+ * whose Fast rows stop at Medium, `high` plus Fast selects the regular High row
214
+ * rather than dropping to Medium to keep Fast. A member with no effort rung
215
+ * cannot prove it is not lower, so it ranks behind any rung at or above the
216
+ * request. `none` (the bottom of the ladder) is never "lowered", which leaves
217
+ * the Thinking-off target in charge of that request.
218
+ */
219
+ function closestMember(
220
+ members: ModelCatalogEntry[],
221
+ targets: Record<string, number>,
222
+ effort: string | undefined,
223
+ ): ModelCatalogEntry | undefined {
224
+ const want = effortIndex(effort);
225
+ let best: ModelCatalogEntry | undefined;
226
+ let bestScore: number[] | undefined;
227
+ for (const member of members) {
228
+ const axes = member.familyAxes ?? {};
229
+ const mismatches = Object.entries(targets).filter(([axis, order]) => (axes[axis]?.order ?? 0) !== order).length;
230
+ const have = effortIndex(devinFamilyEffortOf(member));
231
+ const lowered = want > 0 && have < want ? 1 : 0;
232
+ const distance = want < 0 ? 0
233
+ : have < 0 ? 2 * FAMILY_EFFORT_LADDER.length
234
+ : have >= want ? have - want : FAMILY_EFFORT_LADDER.length + (want - have);
235
+ const score = [lowered, mismatches, distance, -have];
236
+ if (!bestScore || lexicallyLess(score, bestScore)) {
237
+ best = member;
238
+ bestScore = score;
239
+ }
240
+ }
241
+ return best;
242
+ }
243
+
244
+ /**
245
+ * Pick the family member for a request. Axes the caller did not ask about
246
+ * (`Fast Mode`, `1M Context`, `Thinking`, `Prompt Cache Retention`, ...) stay
247
+ * where the anchor has them; the anchor is the member the caller named, else
248
+ * the family's catalog default, else the neutral member (every toggle off,
249
+ * effort nearest Medium) for the few families that mark no default. Disabled
250
+ * members are passed over while any enabled one remains, so the pre-flight
251
+ * only reports a tier refusal when the whole family is refused. `includeDisabled`
252
+ * ranks every member, which tells a caller whether a disabled row is itself the
253
+ * best match for the request.
254
+ */
255
+ export function selectDevinFamilyMember(
256
+ members: ModelCatalogEntry[],
257
+ request: DevinVariantRequest,
258
+ anchor?: ModelCatalogEntry,
259
+ options: { includeDisabled?: boolean } = {},
260
+ ): ModelCatalogEntry | undefined {
261
+ const axisNames = new Set(members.flatMap((m) => Object.keys(m.familyAxes ?? {})).filter((a) => !isEffortAxis(a)));
262
+ const base = anchor
263
+ ?? members.find((m) => m.isFamilyDefault)
264
+ ?? closestMember(members, Object.fromEntries([...axisNames].map((a) => [a, 0])), "medium");
265
+ if (!base) return undefined;
266
+ const targets: Record<string, number> = {};
267
+ for (const axis of axisNames) targets[axis] = base.familyAxes?.[axis]?.order ?? 0;
268
+ if (request.fast && axisNames.has(FAST_AXIS)) targets[FAST_AXIS] = 1;
269
+ if (request.longContext && axisNames.has(LONG_CONTEXT_AXIS)) targets[LONG_CONTEXT_AXIS] = 1;
270
+ // `none` means no reasoning. Families like Claude Sonnet 4.6 express that as
271
+ // Thinking off rather than as an effort rung.
272
+ if (request.effort === "none" && axisNames.has(THINKING_AXIS)) targets[THINKING_AXIS] = 0;
273
+ const enabled = options.includeDisabled ? members : members.filter((m) => !m.disabled);
274
+ return closestMember(enabled.length > 0 ? enabled : members, targets, request.effort ?? devinFamilyEffortOf(base));
275
+ }
276
+
132
277
  /**
133
278
  * Degraded-mode ladders, used only before the account catalog is readable.
134
279
  *
@@ -158,14 +303,18 @@ export type DevinUsableModelsResult =
158
303
  models: string[];
159
304
  contextWindows: Record<string, number>;
160
305
  efforts: Record<string, string[]>;
306
+ /** Per base, the effort of the family's catalog default member, when it is on the ladder. */
307
+ defaultEfforts: Record<string, string>;
161
308
  inputModalities: Record<string, string[]>;
162
309
  }
163
310
  | { ok: false; error: "auth" | "http" | "empty" | "unknown"; detail?: string };
164
311
 
165
312
  /**
166
313
  * Fetch the live model roster from Cognition's `GetCascadeModelConfigs` and
167
- * collapse effort-suffixed variants to base ids. The returned list is the
168
- * authoritative model roster for the signed-in account.
314
+ * collapse variants to base ids. Rows that carry family metadata collapse to
315
+ * their family and read their effort from the family's effort axis; only rows
316
+ * without it (older catalogs) fall back to reading suffixes off the uid. The
317
+ * returned list is the authoritative model roster for the signed-in account.
169
318
  */
170
319
  export async function fetchDevinUsableModels(opts: {
171
320
  apiKey: string;
@@ -187,9 +336,13 @@ export async function fetchDevinUsableModels(opts: {
187
336
  // Skip internal enum constants (e.g. MODEL_GPT_5_2_LOW, MODEL_PRIVATE_*).
188
337
  // Real chat model UIDs are lowercase dashed strings (swe-1-7, gpt-5-6-sol).
189
338
  if (entry.modelUid.startsWith("MODEL_")) continue;
190
- const base = collapseDevinModelUid(entry.modelUid);
339
+ const family = entry.familyUid && entry.familyAxes ? devinFamilyBaseId(entry.familyUid) : undefined;
340
+ const base = family ?? collapseDevinModelUid(entry.modelUid);
191
341
  bases.add(base);
192
- const found = devinReasoningRungsOf(entry.modelUid);
342
+ const familyRung = family ? devinFamilyEffortOf(entry) : undefined;
343
+ const found = family
344
+ ? (familyRung && REASONING_RUNG_TOKENS.has(familyRung) ? [familyRung] : [])
345
+ : devinReasoningRungsOf(entry.modelUid);
193
346
  if (found.length > 0) {
194
347
  let set = rungs.get(base);
195
348
  if (!set) { set = new Set(); rungs.set(base, set); }
@@ -222,13 +375,21 @@ export async function fetchDevinUsableModels(opts: {
222
375
  // would draw a picker whose only option is the value already in effect.
223
376
  if (set.size > 1) efforts[base] = sortDevinRungs(set);
224
377
  }
378
+ const defaultEfforts: Record<string, string> = {};
379
+ for (const [base, members] of devinFamiliesOf(catalog)) {
380
+ if (!members.some((member) => member.isFamilyDefault)) continue;
381
+ const selected = selectDevinFamilyMember(members, {});
382
+ const rung = selected && devinFamilyEffortOf(selected);
383
+ if (rung && efforts[base]?.includes(rung)) defaultEfforts[base] = rung;
384
+ }
225
385
  // supportsImages arrives tri-state per catalog row, so the collapse votes:
226
386
  // a row that never asserted field #5 abstains, which keeps an unsuffixed
227
387
  // unknown row from poisoning a base whose effort variants were measured
228
388
  // image-capable. Unanimous measured rows advertise; measured disagreement
229
389
  // advertises nothing, because a single measured false is not outvoted by
230
- // its siblings. One accepted mismatch: resolveWireModelUid prefers the
231
- // plain UID when the catalog lists it, so a base advertised
390
+ // its siblings. One accepted mismatch, only for rows without family
391
+ // metadata: resolveWireModelUid prefers the plain UID when the catalog
392
+ // lists it, so a base advertised
232
393
  // ["text","image"] on variant evidence can still route a no-effort request
233
394
  // to a plain row that never asserted the field.
234
395
  const inputModalities: Record<string, string[]> = {};
@@ -236,7 +397,7 @@ export async function fetchDevinUsableModels(opts: {
236
397
  if (votes.sawTrue && votes.sawFalse) continue;
237
398
  inputModalities[base] = votes.sawTrue ? ["text", "image"] : ["text"];
238
399
  }
239
- return { ok: true, models: [...bases].sort(), contextWindows, efforts, inputModalities };
400
+ return { ok: true, models: [...bases].sort(), contextWindows, efforts, defaultEfforts, inputModalities };
240
401
  } catch (error) {
241
402
  const message = error instanceof Error ? error.message : String(error);
242
403
  if (/unauth|401|invalid token|login/i.test(message)) return { ok: false, error: "auth", detail: message };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Devin reasoning signatures across turns.
3
+ *
4
+ * GetChatMessage returns the turn's reasoning attestation as `delta_signature`
5
+ * (#10) together with `delta_signature_type` (#21) in the same frame, and the
6
+ * native client replays both on the assistant prompt as #12 and #18. Measured
7
+ * live on swe-2-high the pair arrives AFTER the visible answer (reasoning, text,
8
+ * then signature), so the Responses layer stores it as its own signature-only
9
+ * reasoning item behind the thinking-text item. GPT and Gemini rows stream no
10
+ * thinking text at all, only the signature.
11
+ *
12
+ * The type is carried inside the stored signature because the reasoning
13
+ * envelope that round-trips through the client keeps a single signature string.
14
+ * A signature stored before this prefix existed replays without a type.
15
+ */
16
+ import type { OcxAssistantMessage } from "../../types";
17
+ import { isProviderIssuedThinkingSignature } from "../../responses/reasoning-envelope";
18
+
19
+ const TYPED_SIGNATURE_PREFIX = "devin-sig1:";
20
+
21
+ export function encodeDevinSignature(signature: string, signatureType: string | undefined): string {
22
+ return signatureType && !signatureType.includes(":")
23
+ ? `${TYPED_SIGNATURE_PREFIX}${signatureType}:${signature}`
24
+ : signature;
25
+ }
26
+
27
+ export function decodeDevinSignature(stored: string): { signature: string; signatureType?: string } {
28
+ if (!stored.startsWith(TYPED_SIGNATURE_PREFIX)) return { signature: stored };
29
+ const rest = stored.slice(TYPED_SIGNATURE_PREFIX.length);
30
+ const colon = rest.indexOf(":");
31
+ if (colon <= 0) return { signature: stored };
32
+ return { signature: rest.slice(colon + 1), signatureType: rest.slice(0, colon) };
33
+ }
34
+
35
+ /**
36
+ * The assistant turn's reasoning for ChatMessagePrompt #11/#12/#18.
37
+ *
38
+ * All of the turn's thinking text is replayed. A signature rides along only
39
+ * when it covers exactly that text:
40
+ * - one thinking block carrying its own issued signature (a stray
41
+ * signature-only block does not displace it);
42
+ * - at most one unsigned thinking block plus exactly one signature-only block,
43
+ * which is how a single Devin turn arrives once its late #10 frame has been
44
+ * split into its own reasoning item. With no thinking block at all this is a
45
+ * GPT or Gemini row, where the signature is the only reasoning there is.
46
+ * Any other mix (two signed blocks, a signed block beside unsigned text) has no
47
+ * single attestation for the joined text, so the turn is replayed unsigned.
48
+ *
49
+ * `withholdAnthropic` drops an Anthropic signature and keeps the text: the
50
+ * fallback for a Claude turn Cognition refused (see hasAnthropicSignature).
51
+ */
52
+ export function devinAssistantReasoning(
53
+ message: OcxAssistantMessage,
54
+ modelId = "",
55
+ withholdAnthropic = false,
56
+ ): { thinking?: string; signature?: string; signature_type?: string } {
57
+ const blocks = message.content.filter(
58
+ (part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
59
+ );
60
+ const textBlocks = blocks.filter(part => Boolean(part.thinking));
61
+ const signatureOnly = blocks.filter(part => !part.thinking && isProviderIssuedThinkingSignature(part.signature));
62
+ const text = textBlocks.map(part => part.thinking).join("\n");
63
+ let stored: string | undefined;
64
+ if (textBlocks.length === 1 && isProviderIssuedThinkingSignature(textBlocks[0]!.signature)) {
65
+ stored = textBlocks[0]!.signature;
66
+ } else if (textBlocks.length === 0 && signatureOnly.length === 1) {
67
+ stored = signatureOnly[0]!.signature;
68
+ } else if (textBlocks.length === 1 && signatureOnly.length === 1) {
69
+ // Only the late trailer shape attests this text. The Responses parser can
70
+ // fold reasoning around a call into one assistant message, so counting
71
+ // blocks without checking their position can attach an unrelated signature.
72
+ const textIndex = message.content.indexOf(textBlocks[0]!);
73
+ if (message.content[textIndex + 1] === signatureOnly[0]) stored = signatureOnly[0]!.signature;
74
+ }
75
+ let decoded = stored ? decodeDevinSignature(stored) : undefined;
76
+ if (decoded && withholdAnthropic && signatureTypeFor(decoded, modelId) === "anthropic") decoded = undefined;
77
+ return {
78
+ ...(text ? { thinking: text } : {}),
79
+ ...(decoded ? { signature: decoded.signature } : {}),
80
+ ...(decoded?.signatureType ? { signature_type: decoded.signatureType } : {}),
81
+ };
82
+ }
83
+
84
+ /** A stored signature from before its type was recorded falls back to the model being called. */
85
+ function signatureTypeFor(decoded: { signatureType?: string }, modelId: string): string | undefined {
86
+ return decoded.signatureType ?? (/claude/i.test(modelId) ? "anthropic" : undefined);
87
+ }
88
+
89
+ /**
90
+ * True when the mapped history replays a Claude signature. Cognition streams
91
+ * Claude's thinking as a summary while the signature covers the original, so
92
+ * the pair can fail validation: live on claude-opus-5-5 a signed replay of a
93
+ * visible-thinking turn was refused with `invalid_argument` in 5 of 6 tries and
94
+ * a text-only one in none, while a signed replay that is accepted is what lets
95
+ * the model recall its earlier reasoning. The adapter therefore sends the
96
+ * signature and retries a refusal once without it.
97
+ */
98
+ export function hasAnthropicSignature(items: ReadonlyArray<{ signature?: string; signature_type?: string }>, modelId: string): boolean {
99
+ return items.some(item => Boolean(item.signature) && signatureTypeFor({ signatureType: item.signature_type }, modelId) === "anthropic");
100
+ }