mixdog 1.3.3 → 1.3.5

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 (210) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +3 -2
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/agent-room-host.mjs +1 -1
  128. package/src/runtime/shared/bot-hops.mjs +62 -2
  129. package/src/runtime/shared/config.mjs +49 -22
  130. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  131. package/src/runtime/shared/connector-catalog.mjs +16 -8
  132. package/src/runtime/shared/developer-options.mjs +32 -1
  133. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  134. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  135. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  136. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  137. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  138. package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
  139. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  140. package/src/runtime/shared/provider-accounts.mjs +224 -58
  141. package/src/runtime/shared/provider-api-key.mjs +19 -8
  142. package/src/runtime/shared/routine-trigger.mjs +42 -0
  143. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  144. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  145. package/src/runtime/shared/user-data-guard.mjs +30 -2
  146. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  147. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  148. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  149. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  150. package/src/session-runtime/agent-tool-routing.mjs +49 -4
  151. package/src/session-runtime/boot/begin.mjs +3 -2
  152. package/src/session-runtime/boot/tools.mjs +4 -0
  153. package/src/session-runtime/bot-chain.mjs +32 -0
  154. package/src/session-runtime/bot-status.mjs +7 -4
  155. package/src/session-runtime/channel-config-api.mjs +63 -17
  156. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  157. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  158. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  159. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  160. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  162. package/src/session-runtime/owned-automation-run.mjs +33 -12
  163. package/src/session-runtime/provider-auth/login.mjs +7 -0
  164. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  165. package/src/session-runtime/routine-home-turn.mjs +33 -6
  166. package/src/session-runtime/schedule-session-run.mjs +7 -3
  167. package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
  168. package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
  169. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
  170. package/src/session-runtime/services/agent-tool/render.mjs +3 -1
  171. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  172. package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
  173. package/src/session-runtime/services/channel-admin.mjs +66 -18
  174. package/src/session-runtime/services/channel-worker.mjs +1 -1
  175. package/src/session-runtime/services/provider-admin.mjs +128 -25
  176. package/src/session-runtime/services/schedule-access.mjs +14 -0
  177. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  178. package/src/session-runtime/session-create/session-options.mjs +7 -4
  179. package/src/session-runtime/settings-system-api.mjs +16 -1
  180. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  181. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  182. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  183. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  184. package/src/session-runtime/task-session-route.mjs +99 -0
  185. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  186. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  187. package/src/session-runtime/tool-surface.mjs +4 -0
  188. package/src/session-runtime/usage-stats-api.mjs +1 -1
  189. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  190. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  191. package/src/session-runtime/workspace/transitions.mjs +10 -2
  192. package/src/standalone/daemon-session-control.mjs +88 -20
  193. package/src/standalone/daemon.mjs +8 -0
  194. package/src/standalone/session-protocol.mjs +5 -0
  195. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  196. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  197. package/src/standalone/session-service.mjs +13 -0
  198. package/src/standalone/session-tool-output.mjs +8 -5
  199. package/src/tui/app/model-options.mjs +2 -0
  200. package/src/tui/dist/index.mjs +335 -172
  201. package/src/tui/session/notice-surface.mjs +17 -1
  202. package/src/tui/session/session-api/integrations.mjs +1 -0
  203. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  204. package/src/tui/session/session-api/settings.mjs +14 -2
  205. package/src/tui/session/turn-stream.mjs +17 -1
  206. package/src/tui/session/turn.mjs +22 -2
  207. package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
  208. package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
  209. package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
  210. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
@@ -0,0 +1,541 @@
1
+ // Devin model discovery (GetCliModelConfigs) and catalog normalization.
2
+ import { DEVIN_REVIEWED_FAMILIES } from './devin-wire-families.mjs';
3
+ import { DEVIN_ENUMS } from './devin-wire-protobuf.mjs';
4
+ import { DEVIN_DEFAULT_BASE_URL, devinRequestMetadata, normalizeDevinSessionToken } from './devin-wire-session.mjs';
5
+ import { DevinWireError, devinUnary } from './devin-wire-transport.mjs';
6
+
7
+ export const DEVIN_GET_CLI_MODEL_CONFIGS_PATH = '/exa.api_server_pb.ApiServerService/GetCliModelConfigs';
8
+ export const DEVIN_DEFAULT_CONTEXT_WINDOW = 200_000;
9
+ export const DEVIN_DEFAULT_MAX_OUTPUT_TOKENS = 64_000;
10
+
11
+ const { DisplayOption } = DEVIN_ENUMS;
12
+ // Requested for completeness, never surfaced.
13
+ const INTERNAL_DISPLAYS = new Set([DisplayOption.QUICK_REVIEW, DisplayOption.INTERNAL_DEFAULT]);
14
+ // These lanes advertise images but their backend drops them silently.
15
+ const IMAGE_BLIND_UIDS = new Set(['swe-1-6', 'swe-1-6-fast', 'swe-1-6-slow']);
16
+
17
+ const REASONING_LABEL_RE = /think|thinking|minimal|high|medium|low|xhigh|max|reasoning/i;
18
+ const NO_REASONING_LABEL_RE = /\bno thinking\b/i;
19
+
20
+ function supportsReasoning(config) {
21
+ const features = config.modelInfo?.modelFeatures;
22
+ if (features) return features.supportsThinking === true;
23
+ const label = String(config.label || '');
24
+ return !NO_REASONING_LABEL_RE.test(label) && REASONING_LABEL_RE.test(label);
25
+ }
26
+
27
+ // Router slots (`adaptive`) and harness-backed composites (`fusion`, Fusion
28
+ // pairings) dispatch to other models; they are never effort tiers.
29
+ function isRouter(config) {
30
+ return config.modelInfo?.displayOption === DisplayOption.MODEL_ROUTER || config.modelInfo?.isModelRouter === true;
31
+ }
32
+
33
+ function positive(value) {
34
+ const number = Number(value);
35
+ return Number.isFinite(number) && number > 0 ? number : 0;
36
+ }
37
+
38
+ // Effort tiers least to most intensive. Routing keys add 'off' (thinking
39
+ // disabled), which rows expose under Mixdog's name 'none'.
40
+ const THINKING_EFFORTS = Object.freeze(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
41
+ const ROUTING_KEYS = Object.freeze(['off', ...THINKING_EFFORTS]);
42
+ // Ladder of an automatically paired `X` / `X-thinking` family.
43
+ const PAIR_EFFORTS = Object.freeze(['minimal', 'low', 'medium', 'high']);
44
+
45
+ const { ModelDimensionKind } = DEVIN_ENUMS;
46
+ const COST_DENOMINATOR_RE = /(\d+(?:\.\d+)?)\s*([kmb])?/i;
47
+ const COST_DENOMINATOR_SCALE = Object.freeze({ k: 1_000, m: 1_000_000, b: 1_000_000_000 });
48
+
49
+ function costDenominatorTokens(denominator) {
50
+ const match = COST_DENOMINATOR_RE.exec(String(denominator || ''));
51
+ if (match === null) return 1_000_000;
52
+ const scale = match[2] === undefined ? 1 : (COST_DENOMINATOR_SCALE[match[2].toLowerCase()] ?? 1);
53
+ const tokens = Number(match[1]) * scale;
54
+ return tokens > 0 ? tokens : 1_000_000;
55
+ }
56
+
57
+ // Per-million-token rates from cost dimensions. Composite configs append their
58
+ // components' cards after a `Sidekick` marker; reading stops there.
59
+ function modelCost(config) {
60
+ const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
61
+ for (const dimension of config.modelDimensions || []) {
62
+ const label = String(dimension.label || '')
63
+ .trim()
64
+ .toLowerCase();
65
+ if (label === 'sidekick') break;
66
+ if (dimension.kind !== ModelDimensionKind.COST && dimension.kind !== ModelDimensionKind.COST_FUZZY) continue;
67
+ // Float32 values: round off the noise at sub-cent precision.
68
+ const perMillion =
69
+ Math.round(((Number(dimension.value || 0) * 1_000_000) / costDenominatorTokens(dimension.denominator)) * 1e6) /
70
+ 1e6;
71
+ if (label === 'input') cost.input = perMillion;
72
+ else if (label === 'cached input') cost.cacheRead = perMillion;
73
+ else if (label === 'output') cost.output = perMillion;
74
+ }
75
+ return cost;
76
+ }
77
+
78
+ // One config as an uncollapsed model keyed on its wire uid.
79
+ function modelSpec(config, uid, assignModelRouter) {
80
+ const features = config.modelInfo?.modelFeatures;
81
+ const supportsImages =
82
+ (features ? features.supportsImages === true : config.supportsImages === true) && !IMAGE_BLIND_UIDS.has(uid);
83
+ return {
84
+ id: uid,
85
+ name: String(config.label || '').trim() || uid,
86
+ description: String(config.description || '').trim(),
87
+ reasoning: supportsReasoning(config),
88
+ input: supportsImages ? ['text', 'image'] : ['text'],
89
+ // Router configs ship no features; Cascade serves only tool-calling models.
90
+ supportsTools: features ? features.supportsToolCalls === true : true,
91
+ supportsParallelToolCalls: features?.supportsParallelToolCalls === true,
92
+ cost: modelCost(config),
93
+ contextWindow: positive(config.maxTokens) || DEVIN_DEFAULT_CONTEXT_WINDOW,
94
+ maxTokens: positive(config.modelInfo?.maxOutputTokens) || DEVIN_DEFAULT_MAX_OUTPUT_TOKENS,
95
+ ...(assignModelRouter ? { router: true } : {}),
96
+ // Presentation flags, kept only when the server sets them.
97
+ ...(config.isNew ? { isNew: true } : {}),
98
+ ...(config.isBeta ? { isBeta: true } : {}),
99
+ ...(config.isRecommended ? { isRecommended: true } : {}),
100
+ };
101
+ }
102
+
103
+ // A Fusion pairing `fusion-<lead>-sidekick-<sidekick>` is paired by the native
104
+ // client; served here it would run only its lead, so it is not listed.
105
+ function isFusionPairing(uid) {
106
+ return uid.startsWith('fusion-') && uid.indexOf('-sidekick-') > 'fusion-'.length;
107
+ }
108
+
109
+ const FAMILY_EFFORT_KEYS = new Set(['effort', 'reasoning effort']);
110
+ const FAMILY_EFFORT_BY_NAME = Object.freeze({
111
+ none: 'off',
112
+ nothinking: 'off',
113
+ minimal: 'minimal',
114
+ low: 'low',
115
+ medium: 'medium',
116
+ high: 'high',
117
+ xhigh: 'xhigh',
118
+ max: 'max',
119
+ });
120
+
121
+ function slug(text) {
122
+ return String(text)
123
+ .toLowerCase()
124
+ .replace(/[^a-z0-9]+/g, '-')
125
+ .replace(/^-+|-+$/g, '');
126
+ }
127
+
128
+ // Files a config under its server-declared family lane (`modelFamilyMetadata`).
129
+ // Fast service and 1M context are separate lanes; effort is the lane's axis.
130
+ function collectFamilyLane(lanes, config, uid) {
131
+ const metadata = config.modelFamilyMetadata;
132
+ if (!metadata) return;
133
+ const label = String(metadata.modelFamilyLabel || '').trim();
134
+ if (!label) return;
135
+ let effort;
136
+ let thinking;
137
+ let fast = false;
138
+ let oneMillionContext = false;
139
+ for (const entry of metadata.entries || []) {
140
+ const value = entry.value;
141
+ if (value === undefined) continue;
142
+ const key = String(entry.key || '')
143
+ .toLowerCase()
144
+ .replace(/[^a-z0-9]+/g, ' ')
145
+ .trim();
146
+ const order = value.order ?? 0;
147
+ if (key === 'fast mode') fast = order === 1;
148
+ else if (key === 'thinking') thinking = order === 1;
149
+ else if (key === '1m context') oneMillionContext = order === 1;
150
+ else if (FAMILY_EFFORT_KEYS.has(key)) {
151
+ effort =
152
+ FAMILY_EFFORT_BY_NAME[
153
+ String(value.name || '')
154
+ .toLowerCase()
155
+ .replace(/[^a-z0-9]+/g, '')
156
+ ];
157
+ }
158
+ }
159
+ // Paired non-thinking/thinking configs share an effort label; the explicit
160
+ // Thinking axis decides which one is the off route.
161
+ if (thinking === false) effort = 'off';
162
+ const baseId = slug(label);
163
+ if (!baseId) return;
164
+ const laneId = `${baseId}${oneMillionContext ? '-1m' : ''}${fast ? '-fast' : ''}`;
165
+ let lane = lanes.get(laneId);
166
+ if (!lane) {
167
+ lane = {
168
+ id: laneId,
169
+ name: `${label}${oneMillionContext ? ' 1M' : ''}${fast ? ' Fast' : ''}`,
170
+ members: [],
171
+ routing: {},
172
+ };
173
+ lanes.set(laneId, lane);
174
+ }
175
+ lane.members.push(uid);
176
+ if (lane.defaultMember === undefined && (config.isDefaultModelInFamily || metadata.isDefaultModelInFamily)) {
177
+ lane.defaultMember = uid;
178
+ }
179
+ if (effort !== undefined && lane.routing[effort] === undefined) lane.routing[effort] = uid;
180
+ }
181
+
182
+ // Lanes with an effort ladder become families; the server default member is
183
+ // hoisted first. A lane with only an off route stays uncollapsed.
184
+ function dynamicFamilies(lanes) {
185
+ const families = [];
186
+ for (const lane of lanes) {
187
+ const efforts = THINKING_EFFORTS.filter((effort) => lane.routing[effort] !== undefined);
188
+ if (!efforts.length) continue;
189
+ const { defaultMember } = lane;
190
+ const members =
191
+ defaultMember === undefined
192
+ ? lane.members
193
+ : [defaultMember, ...lane.members.filter((uid) => uid !== defaultMember)];
194
+ const defaultLevel =
195
+ defaultMember === undefined ? undefined : efforts.find((effort) => lane.routing[effort] === defaultMember);
196
+ families.push({
197
+ id: lane.id,
198
+ name: lane.name,
199
+ members,
200
+ routing: lane.routing,
201
+ ...(defaultMember !== undefined ? { defaultMember } : {}),
202
+ thinking: {
203
+ mode: 'effort',
204
+ efforts,
205
+ ...(defaultLevel !== undefined ? { defaultLevel } : {}),
206
+ // No wire uid serves the family with thinking off: effort is mandatory.
207
+ ...(lane.routing.off === undefined ? { requiresEffort: true } : {}),
208
+ },
209
+ });
210
+ }
211
+ return families;
212
+ }
213
+
214
+ function isCollapsedSpec(spec) {
215
+ return spec !== undefined && (spec.requestModelId !== undefined || spec.thinking?.effortRouting !== undefined);
216
+ }
217
+
218
+ // Points an already-collapsed model at the family's declared default member
219
+ // when that member is one of its live routes.
220
+ function reconcileDefaultMember(spec, family, presentMembers) {
221
+ const { defaultMember } = family;
222
+ if (defaultMember === undefined || defaultMember === spec.id) return spec;
223
+ const target =
224
+ presentMembers === undefined || presentMembers.has(defaultMember)
225
+ ? defaultMember
226
+ : family.members.find((id) => presentMembers.has(id));
227
+ if (target === undefined || spec.requestModelId === target) return spec;
228
+ const routing = spec.thinking?.effortRouting;
229
+ if (!routing) return spec;
230
+ return ROUTING_KEYS.some((key) => routing[key] === target) ? { ...spec, requestModelId: target } : spec;
231
+ }
232
+
233
+ // Replaces each family's live members with one logical model at the position
234
+ // of the family's first occurrence; other models pass through in order.
235
+ function collapseFamilies(specs, families) {
236
+ const byId = new Map();
237
+ for (const spec of specs) if (!byId.has(spec.id)) byId.set(spec.id, spec);
238
+ const replacement = new Map();
239
+ const familyIdBySpecId = new Map();
240
+
241
+ for (const family of families) {
242
+ const existing = byId.get(family.id);
243
+ const existingCollapsed = isCollapsedSpec(existing);
244
+ const rawPresent = family.members.filter((id) => byId.has(id) && !(id === family.id && existingCollapsed));
245
+ if (!rawPresent.length) {
246
+ // Already collapsed upstream of this table: only the default may move.
247
+ if (existingCollapsed) {
248
+ const refreshed = reconcileDefaultMember(existing, family);
249
+ if (refreshed !== existing) {
250
+ familyIdBySpecId.set(family.id, family.id);
251
+ replacement.set(family.id, refreshed);
252
+ }
253
+ }
254
+ continue;
255
+ }
256
+ for (const id of rawPresent) familyIdBySpecId.set(id, family.id);
257
+ if (existing) familyIdBySpecId.set(family.id, family.id);
258
+ const presentSet = new Set(rawPresent);
259
+ if (existingCollapsed) {
260
+ // Mixed input: the collapsed model wins and raw members are dropped.
261
+ replacement.set(family.id, reconcileDefaultMember(existing, family, presentSet));
262
+ continue;
263
+ }
264
+ const memberSpecs = rawPresent.map((id) => byId.get(id));
265
+ const [firstMember] = memberSpecs;
266
+ const routing = {};
267
+ let hasRouting = false;
268
+ let hasEffortRoute = false;
269
+ for (const key of ROUTING_KEYS) {
270
+ const target = family.routing[key];
271
+ if (target === undefined || !presentSet.has(target)) continue;
272
+ routing[key] = target;
273
+ hasRouting = true;
274
+ if (key !== 'off') hasEffortRoute = true;
275
+ }
276
+ // Routing efforts to a live member means the family reasons.
277
+ const reasoning = memberSpecs.some((spec) => spec.reasoning) || hasEffortRoute;
278
+ const thinking = family.thinking ? { ...family.thinking } : undefined;
279
+ if (thinking && hasRouting) thinking.effortRouting = routing;
280
+ const input = [];
281
+ if (memberSpecs.some((spec) => spec.input.includes('text'))) input.push('text');
282
+ if (memberSpecs.some((spec) => spec.input.includes('image'))) input.push('image');
283
+ const collapsed = {
284
+ ...firstMember,
285
+ id: family.id,
286
+ name: family.name,
287
+ reasoning,
288
+ input,
289
+ contextWindow: Math.max(...memberSpecs.map((spec) => spec.contextWindow)),
290
+ maxTokens: Math.max(...memberSpecs.map((spec) => spec.maxTokens)),
291
+ };
292
+ if (memberSpecs.some((spec) => spec.isProviderDefault === true)) collapsed.isProviderDefault = true;
293
+ const defaultWireId =
294
+ family.defaultMember !== undefined && presentSet.has(family.defaultMember) ? family.defaultMember : rawPresent[0];
295
+ if (defaultWireId === family.id) delete collapsed.requestModelId;
296
+ else collapsed.requestModelId = defaultWireId;
297
+ // A single-uid family keeps `reasoning` but has no tier to select.
298
+ if (reasoning && thinking) collapsed.thinking = thinking;
299
+ else delete collapsed.thinking;
300
+ replacement.set(family.id, collapsed);
301
+ }
302
+
303
+ if (!replacement.size) return [...specs];
304
+ const emitted = new Set();
305
+ const out = [];
306
+ for (const spec of specs) {
307
+ const familyId = familyIdBySpecId.get(spec.id);
308
+ if (familyId === undefined) {
309
+ out.push(spec);
310
+ continue;
311
+ }
312
+ if (emitted.has(familyId)) continue;
313
+ emitted.add(familyId);
314
+ const familySpec = replacement.get(familyId);
315
+ if (familySpec) out.push(familySpec);
316
+ }
317
+ return out;
318
+ }
319
+
320
+ // Member -> family id for the reviewed table (case-insensitive; a member that
321
+ // is its own family id is not an alias), plus the set of family ids.
322
+ const REVIEWED_MEMBER_FAMILY = new Map();
323
+ const REVIEWED_FAMILY_IDS = new Set();
324
+ for (const family of DEVIN_REVIEWED_FAMILIES) {
325
+ REVIEWED_FAMILY_IDS.add(family.id);
326
+ for (const member of family.members) {
327
+ const key = member.toLowerCase();
328
+ if (member !== family.id && !REVIEWED_MEMBER_FAMILY.has(key)) REVIEWED_MEMBER_FAMILY.set(key, family.id);
329
+ }
330
+ }
331
+ const isReviewedClaim = (id) => REVIEWED_MEMBER_FAMILY.has(id.toLowerCase()) || REVIEWED_FAMILY_IDS.has(id);
332
+
333
+ const PAIR_TOKENS = Object.freeze(['thinking', 'reasoning', 'reasoner']);
334
+ const isTokenChar = (code) => (code >= 48 && code <= 57) || (code >= 97 && code <= 122);
335
+
336
+ // Bare twin id of a thinking variant: a trailing `-thinking`, else the first
337
+ // `-thinking` / `-reasoning` / `-reasoner` token that is not negated
338
+ // (`non-thinking`, `no-thinking`) and not part of a longer word.
339
+ function thinkingTwinBaseId(id) {
340
+ const lower = id.toLowerCase();
341
+ if (lower.endsWith('-thinking')) return id.slice(0, -'-thinking'.length) || undefined;
342
+ for (const token of PAIR_TOKENS) {
343
+ const needle = `-${token}`;
344
+ let from = 0;
345
+ while (from < lower.length) {
346
+ const index = lower.indexOf(needle, from);
347
+ if (index === -1) break;
348
+ const end = index + needle.length;
349
+ let wordStart = index;
350
+ while (wordStart > 0 && isTokenChar(lower.charCodeAt(wordStart - 1))) wordStart--;
351
+ const preceding = lower.slice(wordStart, index);
352
+ if (!isTokenChar(lower.charCodeAt(end)) && preceding !== 'non' && preceding !== 'no') {
353
+ const stripped = id.slice(0, index) + id.slice(end);
354
+ return stripped || undefined;
355
+ }
356
+ from = index + 1;
357
+ }
358
+ }
359
+ return undefined;
360
+ }
361
+
362
+ // Every live `X` + `X-thinking` pair not claimed by the reviewed table, unless
363
+ // both carry real, different prices (distinct SKUs). Off routes to `X`, every
364
+ // effort to the thinking twin.
365
+ function thinkingPairFamilies(specs) {
366
+ const byId = new Map();
367
+ for (const spec of specs) if (!byId.has(spec.id)) byId.set(spec.id, spec);
368
+ const families = [];
369
+ for (const spec of specs) {
370
+ const baseId = thinkingTwinBaseId(spec.id);
371
+ if (baseId === undefined || baseId === spec.id) continue;
372
+ const base = byId.get(baseId);
373
+ if (!base || isReviewedClaim(spec.id) || isReviewedClaim(baseId)) continue;
374
+ const priced = (cost) => cost.input !== 0 || cost.output !== 0;
375
+ if (
376
+ priced(spec.cost) &&
377
+ priced(base.cost) &&
378
+ ['input', 'output', 'cacheRead', 'cacheWrite'].some((key) => spec.cost[key] !== base.cost[key])
379
+ ) {
380
+ continue;
381
+ }
382
+ // A surface already carried by either twin wins. Otherwise no per-class
383
+ // ladder applies: Devin trusts only the thinking surface its catalog
384
+ // reports, and raw configs report none, so every class gets the fallback.
385
+ const baked = spec.thinking ?? base.thinking;
386
+ let surface = { mode: 'budget', efforts: PAIR_EFFORTS };
387
+ if (baked?.efforts?.length) {
388
+ const { effortRouting: _routing, requiresEffort: _required, ...rest } = baked;
389
+ surface = rest;
390
+ }
391
+ const routing = { off: base.id };
392
+ for (const effort of surface.efforts) routing[effort] = spec.id;
393
+ families.push({ id: base.id, name: base.name, members: [base.id, spec.id], routing, thinking: surface });
394
+ }
395
+ return families;
396
+ }
397
+
398
+ function toRow(spec) {
399
+ const row = {
400
+ id: spec.id,
401
+ name: spec.name,
402
+ description: spec.description,
403
+ contextWindow: spec.contextWindow,
404
+ maxOutputTokens: spec.maxTokens,
405
+ supportsVision: spec.input.includes('image'),
406
+ supportsTools: spec.supportsTools,
407
+ supportsParallelToolCalls: spec.supportsParallelToolCalls,
408
+ supportsReasoning: spec.reasoning,
409
+ reasoning: spec.reasoning,
410
+ uid: spec.requestModelId ?? spec.id,
411
+ };
412
+ const thinking = spec.thinking;
413
+ if (thinking?.efforts?.length) {
414
+ const routing = thinking.effortRouting || {};
415
+ // Efforts whose member the account does not serve run on the default uid.
416
+ const levels = [...(routing.off ? ['off'] : []), ...thinking.efforts];
417
+ row.reasoningLevels = levels.map((level) => (level === 'off' ? 'none' : level));
418
+ row.effortUids = Object.fromEntries(
419
+ levels.map((level) => [level === 'off' ? 'none' : level, routing[level] ?? row.uid])
420
+ );
421
+ if (thinking.defaultLevel) row.defaultEffort = thinking.defaultLevel;
422
+ if (thinking.requiresEffort) row.requiresEffort = true;
423
+ }
424
+ if (spec.router) row.router = true;
425
+ if (spec.isProviderDefault) row.isDefault = true;
426
+ for (const flag of ['isNew', 'isBeta', 'isRecommended']) if (spec[flag]) row[flag] = true;
427
+ return row;
428
+ }
429
+
430
+ // Catalog rows for the account, sorted by id. Each enabled, visible uid
431
+ // becomes a model; effort siblings collapse into one logical model, from
432
+ // server family metadata first, then the reviewed family table, then live
433
+ // `X` / `X-thinking` pairs. `defaultModelUid` (the account's starting model,
434
+ // carried on fetchDevinModels' result) marks the default row and, when it is
435
+ // one effort tier of a family, that family's default effort and uid.
436
+ export function normalizeDevinModels(configs, { defaultModelUid = configs?.defaultModelUid } = {}) {
437
+ const accountDefaultUid = String(defaultModelUid || '').trim();
438
+ const specs = [];
439
+ const seen = new Set();
440
+ const lanes = new Map();
441
+ for (const config of configs || []) {
442
+ if (!config || config.disabled === true) continue;
443
+ const displayOption = config.modelInfo?.displayOption ?? DisplayOption.UNSPECIFIED;
444
+ if (INTERNAL_DISPLAYS.has(displayOption)) continue;
445
+ const uid = String(config.modelUid || '').trim();
446
+ if (!uid || seen.has(uid) || isFusionPairing(uid)) continue;
447
+ seen.add(uid);
448
+ const router = isRouter(config);
449
+ // Harness-less routers (`adaptive`) need AssignModel; harness-backed
450
+ // composites (`fusion`) are themselves chat uids.
451
+ const assignModelRouter = router && (config.modelInfo?.harnessUids?.length ?? 0) === 0;
452
+ const spec = modelSpec(config, uid, assignModelRouter);
453
+ if (uid === accountDefaultUid) spec.isProviderDefault = true;
454
+ specs.push(spec);
455
+ // A router dispatches; it is never an effort tier of a family.
456
+ if (!router) collectFamilyLane(lanes, config, uid);
457
+ }
458
+
459
+ const declared = dynamicFamilies(lanes.values());
460
+ let models = declared.length ? collapseFamilies(specs, declared) : specs;
461
+ models = collapseFamilies(models, DEVIN_REVIEWED_FAMILIES);
462
+ const pairs = thinkingPairFamilies(models);
463
+ if (pairs.length) models = collapseFamilies(models, pairs);
464
+
465
+ // The account default outranks its family's own default when an effort
466
+ // routes to it: the family starts on that uid and effort.
467
+ const index = models.findIndex((spec) => spec.isProviderDefault && spec.thinking?.effortRouting);
468
+ if (index !== -1) {
469
+ const family = models[index];
470
+ const effort = THINKING_EFFORTS.find((level) => family.thinking.effortRouting[level] === accountDefaultUid);
471
+ if (effort !== undefined) {
472
+ const next = { ...family, thinking: { ...family.thinking, defaultLevel: effort } };
473
+ if (family.id === accountDefaultUid) delete next.requestModelId;
474
+ else next.requestModelId = accountDefaultUid;
475
+ models[index] = next;
476
+ }
477
+ }
478
+ return models.map(toRow).sort((a, b) => a.id.localeCompare(b.id));
479
+ }
480
+
481
+ // Whole-discovery deadline, shared by the native and legacy requests.
482
+ export const DEVIN_DISCOVERY_TIMEOUT_MS = 5_000;
483
+ // The two-row fallback seed native discovery returns for seats it cannot serve.
484
+ const SEED_UIDS = new Set(['swe-1-6', 'swe-1-6-fast']);
485
+
486
+ // Raw ClientModelConfig objects for the account; the array carries the
487
+ // account's starting model as `defaultModelUid` when the server names one,
488
+ // which normalizeDevinModels picks up. Discovery authenticates with
489
+ // the session token (not the user JWT). The native dev-channel identity is
490
+ // asked first; when it fails, returns nothing usable, or returns only the
491
+ // fallback seed, the legacy editor identity with the raw token is asked, and
492
+ // its catalog wins only when native failed or it is strictly larger.
493
+ // Throws when both fail; an empty array means both answered with no models.
494
+ export async function fetchDevinModels({ sessionToken, fetchFn = fetch, signal, baseUrl = DEVIN_DEFAULT_BASE_URL }) {
495
+ const token = String(sessionToken || '');
496
+ if (!token) {
497
+ throw new DevinWireError('Devin session token is missing', { code: 'unauthenticated', status: 401 });
498
+ }
499
+ const timeout = AbortSignal.timeout(DEVIN_DISCOVERY_TIMEOUT_MS);
500
+ const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
501
+ // One catalog attempt: { configs, rows } on success, { error } on failure.
502
+ const attempt = async (metadata) => {
503
+ try {
504
+ const response = await devinUnary({
505
+ baseUrl,
506
+ path: DEVIN_GET_CLI_MODEL_CONFIGS_PATH,
507
+ requestType: 'GetCliModelConfigsRequest',
508
+ responseType: 'GetCliModelConfigsResponse',
509
+ message: { metadata },
510
+ fetchFn,
511
+ signal: requestSignal,
512
+ });
513
+ const configs = response.clientModelConfigs || [];
514
+ const defaultModelUid = String(response.defaultOverrideModelConfig?.modelUid || '').trim();
515
+ if (defaultModelUid) configs.defaultModelUid = defaultModelUid;
516
+ return { configs, rows: normalizeDevinModels(configs).length };
517
+ } catch (error) {
518
+ if (signal?.aborted) throw error;
519
+ if (timeout.aborted) {
520
+ return {
521
+ error: new DevinWireError(`Devin model discovery timed out after ${DEVIN_DISCOVERY_TIMEOUT_MS} ms`, {
522
+ code: 'unavailable',
523
+ retryable: true,
524
+ }),
525
+ };
526
+ }
527
+ return { error };
528
+ }
529
+ };
530
+
531
+ const native = await attempt(
532
+ devinRequestMetadata({ apiKey: normalizeDevinSessionToken(token), client: 'discovery' })
533
+ );
534
+ const nativeSeedOnly = native.rows > 0 && normalizeDevinModels(native.configs).every((row) => SEED_UIDS.has(row.id));
535
+ if (native.rows > 0 && !nativeSeedOnly) return native.configs;
536
+
537
+ const legacy = await attempt(devinRequestMetadata({ apiKey: token, client: 'legacy' }));
538
+ const chosen = !legacy.error && (native.error || legacy.rows > native.rows) ? legacy : native;
539
+ if (chosen.error) throw chosen.error;
540
+ return chosen.configs;
541
+ }