mixdog 1.2.0 → 1.2.1

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 (201) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -0
  4. package/src/defaults/skills/pptx/SKILL.md +6 -6
  5. package/src/defaults/skills/pptx/references/review.md +6 -4
  6. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  7. package/src/defaults/skills/setup/references/actions.md +13 -3
  8. package/src/defaults/skills/visualize/SKILL.md +12 -9
  9. package/src/rules/shared/60-verification.md +3 -1
  10. package/src/rules/shared/70-delivery.md +4 -2
  11. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  12. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  13. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  14. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  15. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  16. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
  17. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
  20. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  21. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
  25. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  27. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  28. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  29. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  31. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  33. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  35. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  36. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  37. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  38. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
  44. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  46. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  48. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  49. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
  50. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  53. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  54. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  55. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  56. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  57. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  58. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  59. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  60. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  61. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  62. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  63. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  64. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  66. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  68. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  71. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  77. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  78. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  79. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  80. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  82. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
  84. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  85. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  86. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  87. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
  89. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  92. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  99. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  102. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  104. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  106. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  107. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  108. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  110. package/src/runtime/attachments/store.d.mts +3 -0
  111. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  112. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  113. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  114. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  115. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  116. package/src/runtime/memory/lib/memory.mjs +1 -1
  117. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  118. package/src/runtime/shared/atomic-file.mjs +25 -4
  119. package/src/runtime/shared/extension-scopes.mjs +5 -3
  120. package/src/runtime/shared/file-lock.mjs +37 -11
  121. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  122. package/src/runtime/shared/llm/model-catalog.mjs +0 -12
  123. package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
  124. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  125. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  126. package/src/runtime/shared/pristine-execution.mjs +28 -4
  127. package/src/runtime/shared/provider-accounts.mjs +124 -20
  128. package/src/runtime/shared/provider-api-key.mjs +12 -0
  129. package/src/runtime/shared/shell-display.mjs +9 -3
  130. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  131. package/src/runtime/shared/skill-mentions.mjs +133 -0
  132. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  133. package/src/runtime/shared/tool-primitives.mjs +35 -2
  134. package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
  135. package/src/runtime/shared/worker-requests.mjs +15 -2
  136. package/src/session-runtime/automation-agents.mjs +8 -2
  137. package/src/session-runtime/boot/apis.mjs +55 -0
  138. package/src/session-runtime/boot/config.mjs +5 -2
  139. package/src/session-runtime/boot/facade.mjs +1 -0
  140. package/src/session-runtime/boot/infrastructure.mjs +29 -18
  141. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  142. package/src/session-runtime/conversation-import/service.mjs +369 -0
  143. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  144. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  145. package/src/session-runtime/mcp-glue/connect.mjs +57 -10
  146. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  147. package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
  148. package/src/session-runtime/mcp-glue.mjs +22 -2
  149. package/src/session-runtime/new-session-config.mjs +3 -2
  150. package/src/session-runtime/notification-bus.mjs +9 -2
  151. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  152. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  153. package/src/session-runtime/provider-readiness.mjs +16 -10
  154. package/src/session-runtime/resource-extension-sync.mjs +20 -5
  155. package/src/session-runtime/resource-mcp-api.mjs +101 -77
  156. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  157. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  158. package/src/session-runtime/resource-skills-api.mjs +23 -2
  159. package/src/session-runtime/schedule-session-run.mjs +1 -1
  160. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  161. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  162. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  163. package/src/session-runtime/services/provider-admin.mjs +64 -18
  164. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  165. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  166. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  167. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  168. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  169. package/src/session-runtime/skills-api.mjs +7 -3
  170. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  171. package/src/session-runtime/webhook-session-run.mjs +16 -4
  172. package/src/skill-script-entry.mjs +36 -0
  173. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  174. package/src/standalone/daemon.mjs +41 -4
  175. package/src/standalone/session-protocol.mjs +5 -0
  176. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  177. package/src/tui/App.jsx +9 -1
  178. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  179. package/src/tui/app/app-view.jsx +6 -0
  180. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  181. package/src/tui/app/model-options.mjs +2 -0
  182. package/src/tui/app/skill-mention-palette.mjs +88 -0
  183. package/src/tui/app/slash-palette.mjs +23 -7
  184. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  185. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  186. package/src/tui/app/use-prompt-state.mjs +5 -0
  187. package/src/tui/components/Message.jsx +22 -1
  188. package/src/tui/components/PromptInput.jsx +21 -3
  189. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  190. package/src/tui/components/TranscriptItem.jsx +9 -1
  191. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  192. package/src/tui/components/user-message-skills.mjs +38 -0
  193. package/src/tui/dist/index.mjs +507 -171
  194. package/src/tui/session/queue-helpers.mjs +7 -2
  195. package/src/tui/session/session-api/extensions.mjs +4 -2
  196. package/src/tui/session/session-api/settings.mjs +7 -0
  197. package/src/tui/session/session-flow/drain.mjs +14 -4
  198. package/src/tui/session/tui-steering-persist.mjs +25 -9
  199. package/src/tui/session/turn.mjs +17 -0
  200. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  201. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -0,0 +1,159 @@
1
+ // Command Code credentials: a non-expiring Provider API key issued by the
2
+ // Studio CLI sign-in. There is no refresh grant — a 401 means the user signs
3
+ // in again. Stored in Mixdog's own token file (command-code-oauth.json), one
4
+ // file per connected account.
5
+ import { readFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
6
+ import { join, resolve } from 'node:path';
7
+ import { getPluginData } from '../config.mjs';
8
+ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
9
+ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
10
+ import { accountIdentityFields, fileMtimeMs, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
11
+ import { modelProxyClientMode } from '../../../shared/model-proxy-mode.mjs';
12
+
13
+ export const PROVIDER_ID = 'command-code-oauth';
14
+ export const STUDIO_ORIGIN = 'https://commandcode.ai';
15
+ // The only origin the API key is ever sent to.
16
+ export const API_ORIGIN = 'https://api.commandcode.ai';
17
+ export const PROVIDER_BASE_URL = `${API_ORIGIN}/provider/v1`;
18
+ export const WHOAMI_URL = `${API_ORIGIN}/alpha/whoami`;
19
+ export const CREDITS_URL = `${API_ORIGIN}/alpha/billing/credits`;
20
+ export const LOGIN_TIMEOUT_MS = 120_000;
21
+ const REQUEST_TIMEOUT_MS = 15_000;
22
+ const MAX_API_KEY_LEN = 512;
23
+
24
+ export function getOwnTokenPath() {
25
+ const bound = boundProviderAuthPath(PROVIDER_ID);
26
+ if (bound) return resolve(bound);
27
+ const explicit = process.env.COMMAND_CODE_OAUTH_CREDENTIALS_PATH;
28
+ if (explicit) return resolve(explicit);
29
+ const dir = getPluginData();
30
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
+ return join(dir, 'command-code-oauth.json');
32
+ }
33
+
34
+ const text = (value) => (typeof value === 'string' ? value.trim() : '');
35
+
36
+ /** A key is one printable-ASCII token of bounded length; anything else is rejected before use. */
37
+ export function isWellFormedApiKey(value) {
38
+ return typeof value === 'string' && value.length <= MAX_API_KEY_LEN && /^[\x21-\x7e]+$/.test(value);
39
+ }
40
+
41
+ export function loadTokens() {
42
+ if (modelProxyClientMode()) return null;
43
+ const path = getOwnTokenPath();
44
+ if (!existsSync(path)) return null;
45
+ try {
46
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
47
+ const apiKey = text(raw?.apiKey);
48
+ if (!apiKey) return null;
49
+ return { apiKey, userId: text(raw.userId), userName: text(raw.userName), mtimeMs: fileMtimeMs(path) };
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export function saveTokens({ apiKey, userId, userName }) {
56
+ const tokens = { apiKey, userId: text(userId), userName: text(userName) };
57
+ writeJsonAtomicSync(getOwnTokenPath(), tokens, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
58
+ return tokens;
59
+ }
60
+
61
+ export function commandCodeReauthError(detail = '') {
62
+ return Object.assign(
63
+ new Error(
64
+ `[command-code-oauth] API key rejected (401) — open /providers in mixdog to sign in again${detail ? `: ${detail}` : ''}`
65
+ ),
66
+ { httpStatus: 401, status: 401, reauthRequired: true }
67
+ );
68
+ }
69
+
70
+ /**
71
+ * GET a Command Code API endpoint with the key. Refuses any URL outside
72
+ * API_ORIGIN and never follows redirects, so the key cannot leave that host.
73
+ */
74
+ export async function requestCommandCodeJson(url, apiKey, { fetchImpl = fetch, signal = null } = {}) {
75
+ const target = new URL(url);
76
+ if (target.origin !== API_ORIGIN)
77
+ throw new Error(`[command-code-oauth] refusing to send the API key to ${target.host}`);
78
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
79
+ const res = await fetchImpl(target.toString(), {
80
+ method: 'GET',
81
+ headers: { Accept: 'application/json', Authorization: `Bearer ${apiKey}` },
82
+ redirect: 'error',
83
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
84
+ });
85
+ const body = await res.text().catch(() => '');
86
+ if (!res.ok) {
87
+ const detail = scrubOAuthSecrets(body, [apiKey]).trim().slice(0, 300);
88
+ if (res.status === 401) throw commandCodeReauthError(detail);
89
+ throw Object.assign(
90
+ new Error(`[command-code-oauth] ${target.pathname} ${res.status}${detail ? `: ${detail}` : ''}`),
91
+ { status: res.status, httpStatus: res.status }
92
+ );
93
+ }
94
+ try {
95
+ return JSON.parse(body);
96
+ } catch {
97
+ throw new Error(`[command-code-oauth] ${target.pathname} returned invalid JSON`);
98
+ }
99
+ }
100
+
101
+ /** The account behind a key: whoami's user id is the stable account identity. */
102
+ export async function fetchCommandCodeIdentity(apiKey, options = {}) {
103
+ const body = await requestCommandCodeJson(WHOAMI_URL, apiKey, options);
104
+ const user = body?.user;
105
+ const userId = text(user?.id);
106
+ if (!userId) throw new Error('[command-code-oauth] whoami returned no user id');
107
+ return { userId, userName: text(user?.userName) };
108
+ }
109
+
110
+ export function hasCommandCodeOAuthCredentials() {
111
+ try {
112
+ return Boolean(loadTokens()?.apiKey);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ export function describeCommandCodeOAuthCredentials() {
119
+ try {
120
+ const tokens = loadTokens();
121
+ if (!tokens) {
122
+ return {
123
+ authenticated: false,
124
+ usable: false,
125
+ refreshable: false,
126
+ reauthRequired: false,
127
+ status: 'Not Set',
128
+ detail: 'Mixdog token store',
129
+ };
130
+ }
131
+ return {
132
+ authenticated: true,
133
+ usable: true,
134
+ refreshable: false,
135
+ reauthRequired: false,
136
+ status: 'Valid',
137
+ detail: 'Mixdog token store; API key',
138
+ expiresAt: 0,
139
+ ...accountIdentityFields({ id: tokens.userId }),
140
+ ...(tokens.userName ? { accountId: tokens.userName } : {}),
141
+ };
142
+ } catch (err) {
143
+ return {
144
+ authenticated: false,
145
+ usable: false,
146
+ refreshable: false,
147
+ reauthRequired: false,
148
+ status: 'Error',
149
+ detail: String(err?.message || err).slice(0, 200),
150
+ };
151
+ }
152
+ }
153
+
154
+ export function forgetCommandCodeOAuthCredentials() {
155
+ const path = getOwnTokenPath();
156
+ if (!existsSync(path)) return { removed: false };
157
+ unlinkSync(path);
158
+ return { removed: true };
159
+ }
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Command Code provider (commandcode.ai Provider API).
3
+ *
4
+ * Credentials are a Studio-issued API key (command-code-oauth-tokens.mjs).
5
+ * Each model is served on the endpoint its /models row declares: Chat
6
+ * Completions through the OpenAI-compatible engine, Messages-only models
7
+ * (Claude) through the Anthropic engine. Both clients are pinned to the
8
+ * Command Code API host. The key never expires and has no refresh grant: a
9
+ * 401 means the user must sign in again.
10
+ */
11
+ import { noteErrorUsage } from './lib/note-error-usage.mjs';
12
+ import { fileMtimeMs, normalizeExpiresAtMs, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
13
+ import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
14
+ import { listCompatModels } from './openai-compat-models.mjs';
15
+ import { AnthropicProvider } from './anthropic.mjs';
16
+ import { toInclusiveOpenCodeGoUsage as toInclusiveUsage } from './opencode-go.mjs';
17
+ import { sanitizeToolPairs } from '../session/context-utils.mjs';
18
+ import { preparePdfTextForProvider } from './media-normalization.mjs';
19
+ import { currentUsageContext } from '../../../shared/llm/usage-context.mjs';
20
+ import { makeModelCache } from './model-cache.mjs';
21
+ import {
22
+ API_ORIGIN,
23
+ CREDITS_URL,
24
+ PROVIDER_BASE_URL,
25
+ PROVIDER_ID,
26
+ commandCodeReauthError,
27
+ getOwnTokenPath,
28
+ loadTokens,
29
+ requestCommandCodeJson,
30
+ } from './command-code-oauth-tokens.mjs';
31
+
32
+ export {
33
+ hasCommandCodeOAuthCredentials,
34
+ describeCommandCodeOAuthCredentials,
35
+ forgetCommandCodeOAuthCredentials,
36
+ } from './command-code-oauth-tokens.mjs';
37
+ export { beginOAuthLogin, loginOAuth } from './command-code-oauth-login.mjs';
38
+
39
+ export const DEFAULT_MODEL = 'deepseek/deepseek-v4-flash';
40
+ const CHAT_ENDPOINT = '/chat/completions';
41
+ const MESSAGES_ENDPOINT = '/messages';
42
+ // The Anthropic SDK appends /v1/messages itself.
43
+ const MESSAGES_BASE_URL = `${API_ORIGIN}/provider`;
44
+ const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
45
+ const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
46
+
47
+ const isUnauthorized = (err) => Number(err?.httpStatus || err?.status) === 401;
48
+
49
+ // The discovered roster (what /models returned) is persisted so the shared
50
+ // sync lookup sees what listModels found.
51
+ const commandCodeModelCache = makeModelCache({
52
+ fileName: 'command-code-oauth-models.json',
53
+ ttlMs: 24 * 60 * 60 * 1000,
54
+ version: 1,
55
+ });
56
+
57
+ /**
58
+ * The wire a /models row is served on: 'chat' when it declares Chat
59
+ * Completions or declares no endpoints at all, 'messages' when it declares
60
+ * only Messages, null when it declares neither.
61
+ */
62
+ export function modelWire(item) {
63
+ if (typeof item?.id !== 'string' || !item.id) return null;
64
+ const endpoints = item.supported_endpoints;
65
+ if (!Array.isArray(endpoints)) return 'chat';
66
+ const declared = endpoints.map((endpoint) => String(endpoint).replace(/^\/?(?:provider\/)?(?:v1\/)?/, '/'));
67
+ if (declared.includes(CHAT_ENDPOINT)) return 'chat';
68
+ if (declared.includes(MESSAGES_ENDPOINT)) return 'messages';
69
+ return null;
70
+ }
71
+
72
+ // Every request either engine makes goes through this fetch: anything outside
73
+ // the Command Code API origin is refused before the key leaves the process,
74
+ // and redirects are never followed (any 3xx is an error).
75
+ function apiOnlyFetch(fetchImpl) {
76
+ return async (input, init) => {
77
+ const url = new URL(String(input?.url ?? input));
78
+ if (url.origin !== API_ORIGIN) {
79
+ throw new Error(`[command-code-oauth] refusing to send the API key to ${url.host}`);
80
+ }
81
+ const res = await (fetchImpl || globalThis.fetch)(input, { ...init, redirect: 'manual' });
82
+ if (res.status >= 300 && res.status < 400) {
83
+ throw new Error(`[command-code-oauth] refusing to follow a redirect from ${url.host}`);
84
+ }
85
+ return res;
86
+ };
87
+ }
88
+
89
+ // Anthropic engine bound to this credential: only the Command Code key is
90
+ // sent (never an ANTHROPIC_AUTH_TOKEN from the environment) and only to the
91
+ // Command Code API host.
92
+ class CommandCodeMessagesEngine extends AnthropicProvider {
93
+ nativePdf = false;
94
+ _createClient(apiKey) {
95
+ const client = super._createClient(apiKey);
96
+ client.authToken = null;
97
+ client.fetch = apiOnlyFetch(this.config.fetchImpl);
98
+ return client;
99
+ }
100
+ }
101
+
102
+ const normalizeUsageError = (err) => {
103
+ if (err && typeof err === 'object') {
104
+ if (err.usage) err.usage = toInclusiveUsage(err.usage);
105
+ if (err.partialUsage) err.partialUsage = toInclusiveUsage(err.partialUsage);
106
+ }
107
+ return err;
108
+ };
109
+
110
+ function rosterRow(item) {
111
+ const contextWindow = Number(item.context_window ?? item.max_context_window ?? item.context_length ?? 0);
112
+ const outputTokens = Number(item.max_output_tokens ?? item.output_tokens ?? 0);
113
+ const name = [item.display_name, item.name].find((v) => typeof v === 'string' && v.trim());
114
+ return {
115
+ id: item.id,
116
+ provider: PROVIDER_ID,
117
+ ...(name ? { name: name.trim() } : {}),
118
+ ...(contextWindow > 0 ? { contextWindow } : {}),
119
+ ...(outputTokens > 0 ? { outputTokens } : {}),
120
+ };
121
+ }
122
+
123
+ export function chooseDefaultModel(ids) {
124
+ const list = ids || [];
125
+ return list.includes(DEFAULT_MODEL) ? DEFAULT_MODEL : list[0] || DEFAULT_MODEL;
126
+ }
127
+
128
+ // Strip the key from an error (message plus any echoed gateway body).
129
+ function scrubErrorSecrets(err, apiKey) {
130
+ if (!err || typeof err !== 'object' || !apiKey) return err;
131
+ const seen = new Set();
132
+ const walk = (obj, depth) => {
133
+ if (!obj || typeof obj !== 'object' || seen.has(obj) || depth > 4) return;
134
+ seen.add(obj);
135
+ for (const k of Object.keys(obj)) {
136
+ const v = obj[k];
137
+ if (typeof v === 'string') {
138
+ if (v.includes(apiKey)) obj[k] = scrubOAuthSecrets(v, [apiKey]);
139
+ } else walk(v, depth + 1);
140
+ }
141
+ };
142
+ if (typeof err.message === 'string') err.message = scrubOAuthSecrets(err.message, [apiKey]);
143
+ walk(err.error, 0);
144
+ walk(err.body, 0);
145
+ return err;
146
+ }
147
+
148
+ // Redact where the transport first surfaces errors (request open and stream
149
+ // iteration), so the shared retry reporter never logs the key.
150
+ function redactClientErrors(client, apiKey) {
151
+ const completions = client?.chat?.completions;
152
+ if (!completions || !apiKey || completions.create?.commandCodeRedacting === apiKey) return;
153
+ const create = completions.create.bind(completions);
154
+ const wrapped = async (...args) => {
155
+ try {
156
+ const stream = await create(...args);
157
+ if (stream && typeof stream[Symbol.asyncIterator] === 'function') {
158
+ const iterate = stream[Symbol.asyncIterator].bind(stream);
159
+ stream[Symbol.asyncIterator] = () => {
160
+ const it = iterate();
161
+ return {
162
+ next: async (...a) => {
163
+ try {
164
+ return await it.next(...a);
165
+ } catch (err) {
166
+ throw scrubErrorSecrets(err, apiKey);
167
+ }
168
+ },
169
+ return: (...a) => it.return?.(...a),
170
+ };
171
+ };
172
+ }
173
+ return stream;
174
+ } catch (err) {
175
+ throw scrubErrorSecrets(err, apiKey);
176
+ }
177
+ };
178
+ wrapped.commandCodeRedacting = apiKey;
179
+ completions.create = wrapped;
180
+ }
181
+
182
+ // Same for the Anthropic client's streaming request (read through
183
+ // `.asResponse()`); every other failure is scrubbed by _shapeError.
184
+ function redactMessagesErrors(client, apiKey) {
185
+ const messages = client?.messages;
186
+ if (!messages || !apiKey || messages.create?.commandCodeRedacting === apiKey) return;
187
+ const create = messages.create.bind(messages);
188
+ const wrapped = (...args) => {
189
+ const request = create(...args);
190
+ const asResponse = request?.asResponse?.bind(request);
191
+ if (asResponse) {
192
+ request.asResponse = () =>
193
+ asResponse().catch((err) => {
194
+ throw scrubErrorSecrets(err, apiKey);
195
+ });
196
+ }
197
+ return request;
198
+ };
199
+ wrapped.commandCodeRedacting = apiKey;
200
+ messages.create = wrapped;
201
+ }
202
+
203
+ function usageStatus(usedFraction) {
204
+ if (usedFraction >= 1) return 'exhausted';
205
+ if (usedFraction >= 0.9) return 'warning';
206
+ return 'ok';
207
+ }
208
+
209
+ function creditWindow({ id, label, longLabel, durationMs }, entry) {
210
+ const cap = Number(entry?.cap);
211
+ const used = Number(entry?.used);
212
+ if (!entry || !Number.isFinite(cap) || cap <= 0 || !Number.isFinite(used) || used < 0) return null;
213
+ const usedPct = Math.min(100, (used / cap) * 100);
214
+ const usedFraction = usedPct / 100;
215
+ const resetAt = normalizeExpiresAtMs(entry.resetAt);
216
+ return {
217
+ id,
218
+ label,
219
+ longLabel,
220
+ source: 'command-code-credits',
221
+ usedPct: Math.round(usedPct * 100) / 100,
222
+ usedFraction,
223
+ remainingFraction: 1 - usedFraction,
224
+ status: usageStatus(usedFraction),
225
+ durationMs,
226
+ ...(resetAt ? { resetAt } : {}),
227
+ };
228
+ }
229
+
230
+ /** Map `/alpha/billing/credits` windowLimits onto the shared quota-window shape. */
231
+ export function quotaWindowsFromCredits(body) {
232
+ const limits = (body?.data ?? body)?.windowLimits;
233
+ return [
234
+ creditWindow({ id: '5h', label: '5H', longLabel: '5 Hours', durationMs: FIVE_HOURS_MS }, limits?.fiveHour),
235
+ creditWindow({ id: '1w', label: '7D', longLabel: 'Weekly', durationMs: WEEK_MS }, limits?.weekly),
236
+ ].filter(Boolean);
237
+ }
238
+
239
+ export class CommandCodeOAuthProvider {
240
+ // No document block on this wire: a PDF is lowered to text before the request is built.
241
+ nativePdf = false;
242
+ // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
243
+ static inputExcludesCache = false;
244
+ name = PROVIDER_ID;
245
+ config;
246
+ tokens = null;
247
+ _inner = null;
248
+ _innerKey = null;
249
+ _defaultModel = null;
250
+ _defaultModelKey = null;
251
+ _messages = null;
252
+ _messagesKey = null;
253
+ // Model id → wire ('chat' | 'messages') from the roster fetched with `_wiresKey`.
254
+ _wires = new Map();
255
+ _wiresKey = null;
256
+
257
+ constructor(config) {
258
+ this.config = config || {};
259
+ this.tokens = loadTokens();
260
+ try {
261
+ preloadOpenAICompatRuntime();
262
+ } catch {
263
+ /* authoritative send surfaces load failures */
264
+ }
265
+ }
266
+
267
+ ensureAuth() {
268
+ // Re-read when the token file changed (sign-in from another process).
269
+ const mtime = fileMtimeMs(getOwnTokenPath());
270
+ if (!mtime) {
271
+ // File gone (signed out / forgotten): drop every cached credential.
272
+ this.tokens = null;
273
+ this._inner = null;
274
+ this._innerKey = null;
275
+ this._messages = null;
276
+ this._messagesKey = null;
277
+ }
278
+ if (!this.tokens || mtime > (this.tokens.mtimeMs || 0)) this.tokens = loadTokens() || this.tokens;
279
+ if (!this.tokens?.apiKey) {
280
+ throw new Error('[command-code-oauth] credentials not found — open /providers in mixdog to sign in first');
281
+ }
282
+ return this.tokens;
283
+ }
284
+
285
+ _ensureInner() {
286
+ const { apiKey } = this.ensureAuth();
287
+ if (this._inner && this._innerKey === apiKey) return this._inner;
288
+ const { fetchImpl, baseURL: _ignored, ...config } = this.config;
289
+ const inner = new OpenAICompatProvider(PROVIDER_ID, {
290
+ ...config,
291
+ apiKey,
292
+ // Fixed: the key is only ever sent to the Command Code API host.
293
+ baseURL: PROVIDER_BASE_URL,
294
+ // Explicit nulls keep OPENAI_ORG_ID / OPENAI_PROJECT_ID out of the requests.
295
+ clientOptions: { organization: null, project: null, fetch: apiOnlyFetch(fetchImpl) },
296
+ extraHeaders: { Accept: 'application/json' },
297
+ });
298
+ inner._fetchModelItems = ({ signal } = {}) => this._fetchRoster(apiKey, signal);
299
+ redactClientErrors(inner.client, apiKey);
300
+ this._inner = inner;
301
+ this._innerKey = apiKey;
302
+ return inner;
303
+ }
304
+
305
+ // The Anthropic engine for Messages-only models, on the same key and host.
306
+ _ensureMessages(apiKey) {
307
+ if (this._messages && this._messagesKey === apiKey) return this._messages;
308
+ const { baseURL: _ignored, ...config } = this.config;
309
+ const engine = new CommandCodeMessagesEngine({
310
+ ...config,
311
+ name: PROVIDER_ID,
312
+ apiKey,
313
+ baseURL: MESSAGES_BASE_URL,
314
+ disableBetaHeaders: true,
315
+ // x-api-key from the SDK plus the bearer form of the same key.
316
+ extraHeaders: { Authorization: `Bearer ${apiKey}` },
317
+ });
318
+ redactMessagesErrors(engine.client, apiKey);
319
+ this._messages = engine;
320
+ this._messagesKey = apiKey;
321
+ return engine;
322
+ }
323
+
324
+ // Servable rows from /models; only the latest fetch with the still-active
325
+ // key publishes the roster and the per-model wire map.
326
+ async _fetchRoster(apiKey, signal = null) {
327
+ const publish = commandCodeModelCache.beginSave(() => loadTokens()?.apiKey === apiKey);
328
+ const body = await requestCommandCodeJson(`${PROVIDER_BASE_URL}/models`, apiKey, {
329
+ fetchImpl: this.config.fetchImpl,
330
+ signal,
331
+ });
332
+ let list = [];
333
+ if (Array.isArray(body?.data)) list = body.data;
334
+ else if (Array.isArray(body)) list = body;
335
+ const items = list.filter((item) => modelWire(item));
336
+ publish(items.map(rosterRow));
337
+ if (this.tokens?.apiKey === apiKey) {
338
+ this._wires = new Map(items.map((item) => [item.id, modelWire(item)]));
339
+ this._wiresKey = apiKey;
340
+ }
341
+ return items;
342
+ }
343
+
344
+ // The wire for a model, from this credential's roster (fetched once when
345
+ // missing). A model the roster does not know, or a roster that cannot be
346
+ // read right now, stays on Chat Completions; a rejected key is surfaced.
347
+ async _wireFor(inner, model) {
348
+ if (this._wiresKey !== inner.apiKey) {
349
+ try {
350
+ await listCompatModels(inner, { throwOnError: true });
351
+ } catch (err) {
352
+ if (isUnauthorized(err)) throw err;
353
+ }
354
+ }
355
+ return (this._wiresKey === inner.apiKey && this._wires.get(model)) || 'chat';
356
+ }
357
+
358
+ // A rejected key needs a fresh sign-in; the gateway's own detail stays attached.
359
+ _shapeError(err, apiKey) {
360
+ noteErrorUsage(err);
361
+ scrubErrorSecrets(err, apiKey);
362
+ if (isUnauthorized(err) && !err?.reauthRequired) return commandCodeReauthError(String(err?.message || ''));
363
+ return err;
364
+ }
365
+
366
+ async _pickDefaultModel(inner) {
367
+ if (this._defaultModel && this._defaultModelKey === this._innerKey) return this._defaultModel;
368
+ const rows = await listCompatModels(inner, { throwOnError: true });
369
+ this._defaultModel = chooseDefaultModel(rows.map((row) => row.id));
370
+ this._defaultModelKey = this._innerKey;
371
+ return this._defaultModel;
372
+ }
373
+
374
+ async send(messages, model, tools, sendOpts) {
375
+ let inner;
376
+ try {
377
+ inner = this._ensureInner();
378
+ } catch (err) {
379
+ throw this._shapeError(err, this.tokens?.apiKey);
380
+ }
381
+ const apiKey = this._innerKey;
382
+ // The model's declared endpoint picks the wire; a caller cannot move a
383
+ // model to Responses.
384
+ const { compatWireApi: _wire, responsesTransport: _transport, ...opts } = sendOpts || {};
385
+ try {
386
+ const useModel = model || (await this._pickDefaultModel(inner));
387
+ if ((await this._wireFor(inner, useModel)) === 'messages') {
388
+ return await this._sendMessages(apiKey, messages, useModel, tools, opts);
389
+ }
390
+ // _doSend directly: the compat wrapper's 401 handler reloads a static
391
+ // config key, which does not apply to this credential.
392
+ return await inner._doSend(messages, useModel, tools, opts);
393
+ } catch (err) {
394
+ throw this._shapeError(err, apiKey);
395
+ }
396
+ }
397
+
398
+ // Messages wire. Same request preparation as the Anthropic engine's own
399
+ // send, but straight to _doSend: its 401 handler re-reads a static config
400
+ // key and replays, which does not apply to this credential. Anthropic usage
401
+ // excludes cached input; it is reported in this provider's inclusive form.
402
+ async _sendMessages(apiKey, messages, model, tools, opts) {
403
+ const engine = this._ensureMessages(apiKey);
404
+ const prepared = await preparePdfTextForProvider(sanitizeToolPairs(messages), engine);
405
+ const identity = currentUsageContext();
406
+ if (identity) identity.normalizeAbandonedUsage = toInclusiveUsage;
407
+ try {
408
+ const result = await engine._doSend(prepared, model, tools, opts);
409
+ return result?.usage ? { ...result, usage: toInclusiveUsage(result.usage) } : result;
410
+ } catch (err) {
411
+ throw normalizeUsageError(err);
412
+ }
413
+ }
414
+
415
+ async listModels() {
416
+ let inner;
417
+ try {
418
+ inner = this._ensureInner();
419
+ return await listCompatModels(inner, { throwOnError: true });
420
+ } catch (err) {
421
+ throw this._shapeError(err, this.tokens?.apiKey);
422
+ }
423
+ }
424
+
425
+ /** 5-hour and weekly credit windows for this account's key. */
426
+ async getUsageSnapshot() {
427
+ const { apiKey, userId } = this.ensureAuth();
428
+ const body = await requestCommandCodeJson(CREDITS_URL, apiKey, { fetchImpl: this.config.fetchImpl });
429
+ const quotaWindows = quotaWindowsFromCredits(body);
430
+ if (!quotaWindows.length) return null;
431
+ return {
432
+ provider: PROVIDER_ID,
433
+ model: null,
434
+ source: 'command-code-credits',
435
+ fetchedAt: Date.now(),
436
+ quotaWindows,
437
+ ...(userId ? { accountId: userId } : {}),
438
+ };
439
+ }
440
+
441
+ async isAvailable() {
442
+ return loadTokens() !== null;
443
+ }
444
+ }
@@ -4,7 +4,7 @@ import { createRemoteVersionSource } from './npm-cli-version.mjs';
4
4
  // documented version endpoint; the official installer script embeds the
5
5
  // current build (…/lab/<build>/…), parsed best-effort. Any format change or
6
6
  // failure falls back to the floor.
7
- export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.01-e373342';
7
+ export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.12-d47ca18';
8
8
  const BUILD_PATTERN = /downloads\.cursor\.com\/lab\/(\d{4}\.\d{2}\.\d{2}-[0-9a-f]{6,40})\//;
9
9
  export const CURSOR_INSTALL_URL = 'https://cursor.com/install';
10
10
 
@@ -4,6 +4,7 @@
4
4
  // runtime reuses the existing OpenAI-compat / Anthropic engines, constructed
5
5
  // under the custom id so model routes and provider identity stay `custom-…`.
6
6
 
7
+ import { resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
7
8
  import { effectiveCustomModelRows, normalizeCustomProviderConfig } from './custom-provider-config.mjs';
8
9
  import { ANTHROPIC_VERSION } from './lib/anthropic-models.mjs';
9
10
  import { enrichModels } from './model-catalog.mjs';
@@ -39,8 +40,11 @@ function withCustomModels(Base, discover) {
39
40
  customConfig = null;
40
41
  customDiscoveryError = null;
41
42
  _customDiscovered = null;
42
- _customApiKey = '';
43
43
  _customKeyless = false;
44
+ // The key for discovery and error redaction, resolved per use like sends.
45
+ _customKey() {
46
+ return this._customKeyless ? '' : resolveProviderApiKey(this.name, this.config);
47
+ }
44
48
  _customRows() {
45
49
  return effectiveCustomModelRows(this.customConfig, this._customDiscovered || [], { providerId: this.name });
46
50
  }
@@ -57,7 +61,7 @@ function withCustomModels(Base, discover) {
57
61
  this.customDiscoveryError = null;
58
62
  } catch (error) {
59
63
  if (signal?.aborted) throw error;
60
- failure = sanitizeDiscoveryError(error, this._customApiKey);
64
+ failure = sanitizeDiscoveryError(error, this._customKey());
61
65
  this.customDiscoveryError = failure;
62
66
  }
63
67
  const rows = this._customRows();
@@ -82,7 +86,7 @@ async function discoverAnthropicModels(provider, signal) {
82
86
  method: 'GET',
83
87
  signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
84
88
  headers: {
85
- ...(provider._customKeyless ? {} : { 'x-api-key': provider.apiKey || '' }),
89
+ ...(provider._customKeyless ? {} : { 'x-api-key': provider._customKey() }),
86
90
  'anthropic-version': ANTHROPIC_VERSION,
87
91
  },
88
92
  });
@@ -141,7 +145,6 @@ export async function createCustomProvider(id, config) {
141
145
  provider = new Custom(providerId, engine);
142
146
  }
143
147
  provider.customConfig = cfg;
144
- provider._customApiKey = apiKey;
145
148
  provider._customKeyless = keyless;
146
149
  provider._applyCustomRows(provider._customRows());
147
150
  return provider;
@@ -1,4 +1,4 @@
1
- import { normalizeAnthropicEffortInput } from './anthropic-effort.mjs';
1
+ import { anthropicEffortLevelsFor, normalizeAnthropicEffortInput } from './anthropic-effort.mjs';
2
2
  import { CUSTOM_EFFORT_LEVELS, customEffortLevels, isCustomProviderName } from './custom-reasoning.mjs';
3
3
  import { codexModelSupportsEffortUpdates } from './openai-oauth-catalog.mjs';
4
4
 
@@ -147,8 +147,15 @@ export function customAutoEffortCapable(provider, protocol, model) {
147
147
  }
148
148
 
149
149
  /** The model auto effort reads its effort options from: a custom provider's
150
- * effective row (its declared levels), the bare id for every other route. */
150
+ * effective row (its declared levels); an Anthropic route's id with the
151
+ * levels the wire builder accepts (provider rows ignored); the bare id for every other route. */
151
152
  export function autoEffortModel(providerName, providerImpl, model) {
153
+ if (ANTHROPIC_PROVIDERS.has(providerName)) {
154
+ // The wire builder ignores provider rows on first-party routes, so the
155
+ // ladder is exactly what it accepts. `customReasoning` keeps an empty
156
+ // list from falling back to a family ladder.
157
+ return { id: model, reasoningLevels: anthropicEffortLevelsFor(model), customReasoning: true };
158
+ }
152
159
  if (!isCustomProviderName(providerName)) return { id: model };
153
160
  return { ...(providerImpl?.getCachedModelInfo?.(model) || {}), id: model };
154
161
  }