@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
@@ -22,9 +22,10 @@ import { validatesRestoredValue } from "./command-code-restored-schema";
22
22
  * A text block that opens with `<tool_call>` is therefore held instead of streamed. It is dropped
23
23
  * when a native call proves it is a duplicate, or restored on an eligible clean MiMo finish when
24
24
  * it names a declared tool with arguments that fit its schema. Other markup is released unchanged.
25
- * MiMo can also append the markup after ordinary prose inside one text block; the stream filter
26
- * splits such a delta at the marker and holds the markup part the same way (#5698; a marker split
27
- * across deltas after prose is still released as text).
25
+ * MiMo can also append the markup after ordinary prose inside one text block. A marker that
26
+ * follows prose can never become a call: its envelope is still held — a matching native call
27
+ * strips it as an echo — and otherwise it is released as the presentation text it is.
28
+ * A leading bare envelope remains eligible under the declared-tool rules.
28
29
  * A malformed envelope that still opens and closes around a declared function name, but that the
29
30
  * strict parser rejects, is dropped instead of released when the native call for that same function
30
31
  * arrives, and on the clean-finish path, so the echo never reaches the client.
@@ -236,8 +237,14 @@ interface TextBlock {
236
237
  state: "probing" | "held" | "queued" | "dropped" | "streaming";
237
238
  ended: boolean;
238
239
  interrupted: boolean;
239
- /** Tool inputs open when the block started; the native call that duplicates it is one of them. */
240
+ /**
241
+ * Tool inputs the block could echo: those open when it started, plus, for a post-prose tail,
242
+ * at most one later input of the envelope's own tool. The block waits only while one of them
243
+ * is still open.
244
+ */
240
245
  candidates: Set<string>;
246
+ /** The one later input a post-prose tail admitted as a candidate, if any. */
247
+ lateCandidate?: string;
241
248
  }
242
249
 
243
250
  interface TextChunk {
@@ -296,7 +303,19 @@ export class CommandCodeToolTextFilter {
296
303
 
297
304
  toolInputStart(id: unknown, name: unknown): AdapterEvent[] {
298
305
  const events = this.breakOpenBlocks();
299
- if (typeof id === "string" && typeof name === "string") this.openInputs.set(id, name);
306
+ if (typeof id === "string" && typeof name === "string") {
307
+ this.openInputs.set(id, name);
308
+ // A post-prose echo can precede the start of the native input it duplicates. An echo
309
+ // duplicates one call, so a complete tail admits one later input of its own tool and no
310
+ // other: unrelated or repeated starts cannot keep extending the wait. Once every candidate
311
+ // closes without a match, matchNative releases the tail as text.
312
+ for (const block of this.held) {
313
+ if (!block.interrupted || block.candidates.size === 0 || block.lateCandidate !== undefined) continue;
314
+ if (looseEnvelopeName(block.markupParts.join(""), this.declared) !== name) continue;
315
+ block.lateCandidate = id;
316
+ block.candidates.add(id);
317
+ }
318
+ }
300
319
  return events;
301
320
  }
302
321
 
@@ -341,54 +360,60 @@ export class CommandCodeToolTextFilter {
341
360
  block = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: false, candidates: new Set(this.openInputs.keys()) };
342
361
  this.blocks.set(key, block);
343
362
  }
344
- // MiMo can append tool-call markup after ordinary prose inside one text block. The probe below
345
- // only recognizes a block that opens with the marker, so a marker arriving after prose would
346
- // reach the client (captured 2026-09-23 from xiaomi/mimo-v2.6-pro: prose, then
347
- // "<tool_call><function=exec>..." echoed by the gateway as one text delta). Split the delta at
348
- // the marker: prose keeps its queued or streamed path, the markup starts a fresh probe block
349
- // and follows the normal hold-and-restore route. A probing block that has consumed nothing but
350
- // whitespace keeps its probe instead, because that probe already holds the marker.
351
- if (block.state !== "held") {
352
- const markerIndex = text.indexOf(TOOL_CALL_MARKER);
353
- const whitespaceLead = markerIndex > 0 && block.state === "probing" && block.probe === ""
354
- && text.slice(0, markerIndex).trim() === "";
355
- // markerIndex === 0 on a probing block is the ordinary hold path; on any other state the
356
- // block is ordinary text and the marker must still start a fresh probe block.
357
- if (!whitespaceLead && (markerIndex > 0 || (markerIndex === 0 && block.state !== "probing"))) {
358
- const prose = markerIndex > 0 ? text.slice(0, markerIndex) : "";
359
- const marked = markerIndex > 0 ? text.slice(markerIndex) : text;
360
- let proseEvents: AdapterEvent[] = [];
361
- if (prose) {
362
- if (block.state === "streaming" && this.head === this.pending.length) {
363
- proseEvents = [{ type: "text_delta", text: prose }];
364
- } else {
365
- if (block.state === "dropped" || block.state === "streaming") {
366
- block = { id: key, markupParts: [], probe: "", bytes: 0, state: "queued", ended: false, interrupted: true, candidates: new Set() };
367
- this.blocks.set(key, block);
368
- }
369
- proseEvents = this.queueProseDelta(block, prose);
370
- this.probeBlockText(block, prose);
371
- }
372
- }
373
- if (block.state === "queued") block.state = "streaming";
374
- this.activeProbes.delete(key);
375
- const probeBlock: TextBlock = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: false, candidates: new Set(this.openInputs.keys()) };
376
- this.blocks.set(key, probeBlock);
377
- return [...boundaryEvents, ...proseEvents, ...this.textDelta(id, marked)];
378
- }
379
- if (markerIndex === -1 && block.state === "streaming" && this.head === this.pending.length) {
380
- return [...boundaryEvents, { type: "text_delta", text }];
363
+ // Probe before retaining so the state transition precedes the marker scan: a delta that
364
+ // carries prose AND a marker must split this visit, not a later one.
365
+ if (block.state === "probing") {
366
+ this.probeBlockText(block, text);
367
+ if (block.state === "probing") this.activeProbes.set(key, block);
368
+ else this.activeProbes.delete(key);
369
+ }
370
+ // Only a bare text block can open protocol markup. Once prose has committed the block, a
371
+ // marker inside the delta starts a held tail instead: it never restores a call, but a
372
+ // matching native call still strips the echo and an unmatched one releases as text.
373
+ if (block.state === "streaming" || block.state === "queued") {
374
+ const markerStart = this.findFreshMarker(text);
375
+ if (markerStart !== undefined) {
376
+ return [
377
+ ...boundaryEvents,
378
+ ...this.enqueueTailProse(block, text.slice(0, markerStart)),
379
+ ...this.openHeldTail(key, text.slice(markerStart)),
380
+ ];
381
381
  }
382
382
  }
383
+ if (block.state === "streaming" && this.head === this.pending.length) {
384
+ return [...boundaryEvents, { type: "text_delta", text }];
385
+ }
383
386
  // Once a duplicate is dropped, later text is a new chunk at its own wire position.
384
387
  if (block.state === "dropped" || block.state === "streaming") {
385
388
  block = { id: key, markupParts: [], probe: "", bytes: 0, state: "queued", ended: false, interrupted: true, candidates: new Set() };
386
389
  this.blocks.set(key, block);
387
390
  }
388
- const preceding = this.makeRoom(encoder.encode(text).byteLength);
389
- this.retain(block, text);
390
- if (block.state === "probing") this.activeProbes.set(key, block);
391
+ const events = this.retainChunk(block, text);
392
+ return [...boundaryEvents, ...events, ...this.limitPending()];
393
+ }
394
+
395
+ /** Find a complete marker or its trailing prefix before either can reach presentation text. */
396
+ private findFreshMarker(text: string): number | undefined {
397
+ const index = text.indexOf(TOOL_CALL_MARKER);
398
+ if (index >= 0) return index;
399
+ for (let length = Math.min(text.length, TOOL_CALL_MARKER.length - 1); length > 0; length--) {
400
+ if (text.endsWith(TOOL_CALL_MARKER.slice(0, length))) return text.length - length;
401
+ }
402
+ return undefined;
403
+ }
404
+
405
+ /** Retain a text fragment on a block and append its pending chunk. Never drains. */
406
+ private retainChunk(block: TextBlock, text: string): AdapterEvent[] {
391
407
  const bytes = encoder.encode(text).byteLength;
408
+ const flushing = this.queuedBytes + bytes > MAX_HELD_TOOL_TEXT_BYTES;
409
+ const preceding = this.makeRoom(bytes);
410
+ // The new block has no pending chunk during the flush, so demote it here too.
411
+ if (flushing && (block.state === "held" || block.state === "probing")) {
412
+ block.state = "queued";
413
+ block.markupParts = [];
414
+ this.activeProbes.delete(block.id);
415
+ }
416
+ this.retain(block, text);
392
417
  const tail = this.pending.at(-1);
393
418
  if (tail?.kind === "chunk" && tail.block === block && this.head < this.pending.length) {
394
419
  tail.parts.push(text);
@@ -399,9 +424,32 @@ export class CommandCodeToolTextFilter {
399
424
  this.queueOperations++;
400
425
  }
401
426
  this.queuedBytes += bytes;
402
- this.probeBlockText(block, text);
403
- if (block.state !== "probing") this.activeProbes.delete(key);
404
- return [...boundaryEvents, ...preceding, ...this.limitPending()];
427
+ return preceding;
428
+ }
429
+
430
+ /**
431
+ * Emit or retain the prose that precedes a mid-prose marker. Streamed text takes the same
432
+ * immediate path it would have without a marker; anything queued stays on the wire order.
433
+ */
434
+ private enqueueTailProse(block: TextBlock, text: string): AdapterEvent[] {
435
+ if (text === "") return [];
436
+ if (block.state === "streaming" && this.head === this.pending.length) {
437
+ return [{ type: "text_delta", text }];
438
+ }
439
+ return this.retainChunk(block, text);
440
+ }
441
+
442
+ /**
443
+ * Open a budgeted probing/held tail for markup or a marker prefix that followed prose. The tail shares the native-call matching of
444
+ * an ordinary held block — a same-content call drops it as an echo — but `interrupted` bars
445
+ * restoration forever: prose-prefixed markup can never mint a call, only disappear or be text.
446
+ */
447
+ private openHeldTail(key: string, text: string): AdapterEvent[] {
448
+ const tail: TextBlock = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: true, candidates: new Set(this.openInputs.keys()) };
449
+ this.blocks.set(key, tail);
450
+ this.probeBlockText(tail, text);
451
+ if (tail.state === "probing") this.activeProbes.set(key, tail);
452
+ return [...this.retainChunk(tail, text), ...this.limitPending()];
405
453
  }
406
454
 
407
455
  textEnd(id: unknown): AdapterEvent[] {
@@ -476,7 +524,9 @@ export class CommandCodeToolTextFilter {
476
524
  // native call is for the function it declares, that call carries the execution, so drop the
477
525
  // echo rather than releasing it as text. A native call for any other tool proves nothing
478
526
  // about this envelope, so it keeps the release-as-text path below.
479
- if (markup === undefined && looseEnvelopeName(text, this.declared) === name) {
527
+ // A held tail (markup after prose) never drops on name alone — its context is ordinary
528
+ // prose the model wrote, so only an exact input match can prove it is an echo.
529
+ if (!block.interrupted && markup === undefined && looseEnvelopeName(text, this.declared) === name) {
480
530
  this.drop(block);
481
531
  block.state = "dropped";
482
532
  this.activeProbes.delete(block.id);
@@ -593,24 +643,6 @@ export class CommandCodeToolTextFilter {
593
643
  }
594
644
  }
595
645
 
596
- /** Route ordinary prose through the queued wire path (shared by the mid-stream marker split). */
597
- private queueProseDelta(block: TextBlock, prose: string): AdapterEvent[] {
598
- const preceding = this.makeRoom(encoder.encode(prose).byteLength);
599
- this.retain(block, prose);
600
- const bytes = encoder.encode(prose).byteLength;
601
- const tail = this.pending.at(-1);
602
- if (tail?.kind === "chunk" && tail.block === block && this.head < this.pending.length) {
603
- tail.parts.push(prose);
604
- tail.bytes += bytes;
605
- this.queueOperations++;
606
- } else {
607
- this.pending.push({ kind: "chunk", block, parts: [prose], bytes });
608
- this.queueOperations++;
609
- }
610
- this.queuedBytes += bytes;
611
- return preceding;
612
- }
613
-
614
646
  private drop(block: TextBlock): void {
615
647
  this.budget.releaseRetained(block.bytes, { kind: "live_transient" });
616
648
  this.queuedBytes = Math.max(0, this.queuedBytes - block.bytes);
@@ -16,6 +16,7 @@ import { parseDataUrl } from "./image";
16
16
  import { createAdapterPhysicalSend } from "./physical-send";
17
17
  import { SendBudgetExhaustedError } from "../lib/upstream-retry";
18
18
  import { CommandCodeToolTextFilter, type CommandCodeDeclaredTools } from "./command-code-tool-text";
19
+ import { EMPTY_COMMAND_CODE_PROJECT_CONTEXT, loadCommandCodeProjectContext } from "./command-code-project-context";
19
20
 
20
21
  function declaredTools(tools: OcxTool[]): CommandCodeDeclaredTools {
21
22
  return new Map(tools.map(tool => [
@@ -610,8 +611,11 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
610
611
  ...(choiceInstruction ? [choiceInstruction] : []),
611
612
  ].join("\n\n"), parsed.modelId);
612
613
  const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
614
+ const projectContext = provider.projectContext === "on"
615
+ ? await loadCommandCodeProjectContext(cwd)
616
+ : EMPTY_COMMAND_CODE_PROJECT_CONTEXT;
613
617
  const body = {
614
- config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null,
618
+ config: await commandCodeConfig(cwd), ...projectContext,
615
619
  permissionMode: "standard", mode: "agent",
616
620
  params: {
617
621
  model: canonicalCommandCodeModelId(parsed.modelId),
@@ -140,6 +140,17 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
140
140
  thinking: { levels: FULL, order: T },
141
141
  },
142
142
  },
143
+ // 260929 Claude Sonnet 5.5: cursor.com/docs/models/claude-sonnet-5-5 lists it (1M max context),
144
+ // but the live GetUsableModels roster does not yet. Shaped like Opus 5.5 (flat effort ids, no
145
+ // thinking variant); re-shape from the roster once it appears.
146
+ "claude-sonnet-5-5": {
147
+ displayName: "Claude Sonnet 5.5",
148
+ window: CONTEXT_1M,
149
+ defaultVariant: "regular",
150
+ variants: {
151
+ regular: { levels: FULL },
152
+ },
153
+ },
143
154
  "claude-sonnet-5": {
144
155
  displayName: "Claude Sonnet 5",
145
156
  window: CONTEXT_1M,
@@ -39,6 +39,9 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
39
39
  // 260923 Opus 5.5: live GetUsableModels roster advertises low..max in both regular and fast forms.
40
40
  "claude-opus-5-5": ["low", "medium", "high", "xhigh", "max"],
41
41
  "claude-opus-5-5-fast": ["low", "medium", "high", "xhigh", "max"],
42
+ // 260929 preemptive Sonnet 5.5: Anthropic documents low..max; flat ids like Opus 5.5 until the
43
+ // live GetUsableModels roster lists the model.
44
+ "claude-sonnet-5-5": ["low", "medium", "high", "xhigh", "max"],
42
45
  "claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
43
46
  "glm-5.2": ["high", "max"],
44
47
  // 260825 live GetUsableModels. gemini-3.6-flash was the first Cursor model exposing
@@ -35,8 +35,18 @@
35
35
  * #5 supports_images bool ← tri-state: absent stays unknown
36
36
  * #18 max_input_tokens varint ← per-account context window
37
37
  * #22 model_uid string ← what `GetChatMessage` accepts
38
+ * #23 model_info ModelInfo { #6 features { #15 supports_thinking },
39
+ * #13 max_output_tokens, #23 model_family_uid }
40
+ * #30 family_metadata ModelFamilyMetadata { #1 family_label,
41
+ * #2 repeated Entry { #1 axis key, #2 { #1 order, #2 name } },
42
+ * #3 is_default_model_in_family }
43
+ * #31 is_default_model_in_family bool
38
44
  * }
39
45
  *
46
+ * #23/#30/#31 were read back from a live 267-row catalog (2026-09-27): every
47
+ * current row names its family, and each family marks one default member.
48
+ * Legacy `MODEL_*` rows carry no #30.
49
+ *
40
50
  * Disabled semantics: TRUE means "this UID exists in the catalog but the
41
51
  * caller's account/tier cannot run inference against it." BYOK models
42
52
  * surface as `disabled: false` so users with their own provider keys still
@@ -91,6 +101,73 @@ export interface ModelCatalogEntry {
91
101
  * (see src/providers/antigravity-models.ts).
92
102
  */
93
103
  supportsImages?: boolean;
104
+ /** ModelInfo #13: the model's own output-token ceiling. */
105
+ maxOutputTokens?: number;
106
+ /** ModelInfo #6.15. */
107
+ supportsThinking?: boolean;
108
+ /** ModelInfo #23, e.g. `swe-1.7` for both `swe-1-7` and `swe-1-7-medium`. */
109
+ familyUid?: string;
110
+ /** ModelFamilyMetadata #1, e.g. `SWE-1.7`. */
111
+ familyLabel?: string;
112
+ /**
113
+ * ModelFamilyMetadata #2: this row's position on each of its family's axes,
114
+ * keyed by axis name (`Effort`, `Reasoning Effort`, `Fast Mode`, `1M Context`,
115
+ * `Thinking`, ...). Toggle axes such as `Fast Mode` carry only an order.
116
+ */
117
+ familyAxes?: Record<string, DevinFamilyAxisValue>;
118
+ /** #31 (or #30.3): the member the family selects when nothing is asked for. */
119
+ isFamilyDefault?: boolean;
120
+ }
121
+
122
+ export interface DevinFamilyAxisValue {
123
+ order: number;
124
+ name?: string;
125
+ }
126
+
127
+ /**
128
+ * Rows whose catalog entry claims image support that the model does not have.
129
+ * Live 2026-09-27: `swe-1-6` answered "NOIMAGE" to a solid red PNG while
130
+ * `swe-1-6-fast` and `swe-2-medium` named the colour, so the image is dropped
131
+ * server-side without an error. Marking it text-only lets the vision fallback
132
+ * describe the image instead of the model silently never seeing it.
133
+ */
134
+ const IMAGE_BLIND_UIDS = new Set(['swe-1-6']);
135
+
136
+ function parseFamilyMetadata(buf: Buffer): { label?: string; axes: Record<string, DevinFamilyAxisValue>; isDefault: boolean } {
137
+ let label: string | undefined;
138
+ let isDefault = false;
139
+ const axes: Record<string, DevinFamilyAxisValue> = {};
140
+ for (const f of iterFields(buf)) {
141
+ if (f.num === 1 && f.wire === 2 && Buffer.isBuffer(f.value)) label = f.value.toString('utf8');
142
+ else if (f.num === 3 && f.wire === 0) isDefault = f.value === 1n;
143
+ else if (f.num === 2 && f.wire === 2 && Buffer.isBuffer(f.value)) {
144
+ let key = '';
145
+ const value: DevinFamilyAxisValue = { order: 0 };
146
+ for (const e of iterFields(f.value)) {
147
+ if (e.num === 1 && e.wire === 2 && Buffer.isBuffer(e.value)) key = e.value.toString('utf8');
148
+ else if (e.num === 2 && e.wire === 2 && Buffer.isBuffer(e.value)) {
149
+ for (const v of iterFields(e.value)) {
150
+ if (v.num === 1 && v.wire === 0) value.order = Number(v.value);
151
+ else if (v.num === 2 && v.wire === 2 && Buffer.isBuffer(v.value)) value.name = v.value.toString('utf8');
152
+ }
153
+ }
154
+ }
155
+ if (key) axes[key] = value;
156
+ }
157
+ }
158
+ return { ...(label ? { label } : {}), axes, isDefault };
159
+ }
160
+
161
+ function parseModelInfo(buf: Buffer): { maxOutputTokens?: number; familyUid?: string; supportsThinking?: boolean } {
162
+ const out: { maxOutputTokens?: number; familyUid?: string; supportsThinking?: boolean } = {};
163
+ for (const f of iterFields(buf)) {
164
+ if (f.num === 13 && typeof f.value === 'bigint' && f.value > 0n) out.maxOutputTokens = Number(f.value);
165
+ else if (f.num === 23 && f.wire === 2 && Buffer.isBuffer(f.value) && f.value.length > 0) out.familyUid = f.value.toString('utf8');
166
+ else if (f.num === 6 && f.wire === 2 && Buffer.isBuffer(f.value)) {
167
+ for (const g of iterFields(f.value)) if (g.num === 15 && g.wire === 0) out.supportsThinking = g.value === 1n;
168
+ }
169
+ }
170
+ return out;
94
171
  }
95
172
 
96
173
  export interface CacheEntry {
@@ -127,6 +204,9 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
127
204
  let disabled = false;
128
205
  let contextWindow = 0;
129
206
  let supportsImages: boolean | undefined;
207
+ let info: ReturnType<typeof parseModelInfo> = {};
208
+ let family: ReturnType<typeof parseFamilyMetadata> | undefined;
209
+ let isFamilyDefault = false;
130
210
  for (const sf of iterFields(f.value as Buffer)) {
131
211
  if (sf.num === 1 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
132
212
  label = (sf.value as Buffer).toString('utf8');
@@ -145,8 +225,15 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
145
225
  contextWindow = Number(sf.value);
146
226
  } else if (sf.num === 22 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
147
227
  modelUid = (sf.value as Buffer).toString('utf8');
228
+ } else if (sf.num === 23 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
229
+ info = parseModelInfo(sf.value);
230
+ } else if (sf.num === 30 && sf.wire === 2 && Buffer.isBuffer(sf.value)) {
231
+ family = parseFamilyMetadata(sf.value);
232
+ } else if (sf.num === 31 && sf.wire === 0) {
233
+ isFamilyDefault = sf.value === 1n;
148
234
  }
149
235
  }
236
+ if (IMAGE_BLIND_UIDS.has(modelUid)) supportsImages = false;
150
237
  if (modelUid.length > 0) {
151
238
  byUid.set(modelUid, {
152
239
  modelUid,
@@ -154,6 +241,10 @@ export function parseCatalogBuffer(buf: Buffer, apiKey: string, host: string): C
154
241
  disabled,
155
242
  ...(contextWindow > 0 ? { contextWindow } : {}),
156
243
  ...(supportsImages !== undefined ? { supportsImages } : {}),
244
+ ...info,
245
+ ...(family?.label ? { familyLabel: family.label } : {}),
246
+ ...(family ? { familyAxes: family.axes } : {}),
247
+ ...(isFamilyDefault || family?.isDefault ? { isFamilyDefault: true } : {}),
157
248
  });
158
249
  }
159
250
  }