mixdog 1.0.12 → 1.1.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.
- package/README.md +75 -181
- package/package.json +4 -2
- package/scripts/build-shipped-history.mjs +131 -0
- package/scripts/lib/run-node-tests.mjs +235 -13
- package/scripts/lib/test-failure-records.mjs +5 -0
- package/scripts/lib/test-failure-summary.mjs +9 -3
- package/scripts/lib/test-spawn-ledger.mjs +19 -0
- package/scripts/prune-embedding-runtime.mjs +9 -5
- package/scripts/test-direct.mjs +16 -1
- package/src/agents/advisor/AGENT.md +38 -0
- package/src/agents/advisor/agent.json +6 -0
- package/src/agents/reviewer/AGENT.md +5 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +381 -0
- package/src/defaults/skills/goal-management/SKILL.md +11 -5
- package/src/defaults/skills/setup/SKILL.md +5 -8
- package/src/defaults/skills/setup/references/actions.md +61 -31
- package/src/defaults/skills/setup/references/surfaces.md +12 -10
- package/src/lib/rules-builder.cjs +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
- package/src/runtime/agent/orchestrator/config.mjs +102 -21
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
- package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
- package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
- package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
- package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
- package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
- package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
- package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
- package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
- package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
- package/src/runtime/channels/lib/config.mjs +3 -8
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
- package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
- package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
- package/src/runtime/computer-bridge/actions.mjs +2 -0
- package/src/runtime/effort-judge/judge-client.mjs +12 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
- package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
- package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
- package/src/runtime/memory/lib/memory-embed.mjs +5 -0
- package/src/runtime/shared/agent-route-config.mjs +50 -2
- package/src/runtime/shared/bounded-file-read.mjs +166 -0
- package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
- package/src/runtime/shared/config.mjs +24 -4
- package/src/runtime/shared/defaults-separation.mjs +263 -0
- package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
- package/src/runtime/shared/llm/model-catalog.mjs +23 -0
- package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
- package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
- package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
- package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
- package/src/runtime/shared/orchestration.mjs +14 -6
- package/src/runtime/shared/projects.mjs +65 -48
- package/src/runtime/shared/provider-accounts.mjs +47 -6
- package/src/runtime/shared/quota-resume-store.mjs +255 -0
- package/src/runtime/shared/shipped-definitions.mjs +275 -0
- package/src/runtime/shared/tool-card-model.golden.json +3 -3
- package/src/runtime/shared/tool-result-summary.mjs +86 -15
- package/src/runtime/shared/transcript-metadata.mjs +2 -0
- package/src/runtime/shared/turn-review-reply.mjs +2 -0
- package/src/runtime/shared/turn-snapshot.mjs +65 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
- package/src/session-runtime/boot/apis.mjs +19 -0
- package/src/session-runtime/boot/config.mjs +4 -0
- package/src/session-runtime/boot/facade.mjs +4 -1
- package/src/session-runtime/boot/infrastructure.mjs +21 -2
- package/src/session-runtime/boot/session.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
- package/src/session-runtime/config-lifecycle.mjs +31 -6
- package/src/session-runtime/goal-facade-api.mjs +4 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
- package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
- package/src/session-runtime/media-api.mjs +32 -22
- package/src/session-runtime/model-recency.mjs +14 -0
- package/src/session-runtime/model-route/route-tuning.mjs +3 -5
- package/src/session-runtime/model-route/set-route.mjs +49 -21
- package/src/session-runtime/model-route-api.mjs +2 -1
- package/src/session-runtime/notification-bus.mjs +31 -6
- package/src/session-runtime/provider-auth/login.mjs +6 -0
- package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
- package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
- package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
- package/src/session-runtime/resource-api.mjs +8 -1
- package/src/session-runtime/runtime-core.mjs +4 -0
- package/src/session-runtime/runtime-facade.mjs +20 -1
- package/src/session-runtime/runtime-review-api.mjs +12 -1
- package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +87 -13
- package/src/session-runtime/services/provider-admin.mjs +256 -63
- package/src/session-runtime/services/seeds.mjs +73 -142
- package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
- package/src/session-runtime/services/usage-dashboard.mjs +7 -0
- package/src/session-runtime/session-address.mjs +125 -0
- package/src/session-runtime/session-create.mjs +57 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
- package/src/session-runtime/settings-compaction-api.mjs +13 -0
- package/src/session-runtime/setup-tool/executor.mjs +92 -40
- package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
- package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
- package/src/session-runtime/tool-surface.mjs +16 -9
- package/src/session-runtime/turn/session-ops.mjs +3 -0
- package/src/session-runtime/turn/share-ops.mjs +4 -1
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
- package/src/session-runtime/usage-stats-api.mjs +15 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
- package/src/standalone/daemon-boot-coordinator.mjs +10 -0
- package/src/standalone/daemon-drain-policy.mjs +9 -0
- package/src/standalone/daemon-stored-session-views.mjs +7 -8
- package/src/standalone/daemon.mjs +21 -5
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client.mjs +52 -7
- package/src/standalone/session-protocol.mjs +8 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
- package/src/standalone/session-service/entries/session-loads.mjs +9 -0
- package/src/standalone/session-service/projection/revisions.mjs +28 -4
- package/src/standalone/session-service/projection.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +140 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
- package/src/standalone/session-service/session-calls.mjs +10 -2
- package/src/standalone/session-service/stored-reader.mjs +26 -1
- package/src/standalone/session-service.mjs +21 -0
- package/src/standalone/session-transport/routes.mjs +2 -0
- package/src/tui/app/doctor.mjs +7 -11
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
- package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
- package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
- package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
- package/src/tui/components/ContextPanel.jsx +5 -1
- package/src/tui/components/Spinner.jsx +34 -319
- package/src/tui/components/SpinnerByline.jsx +256 -0
- package/src/tui/components/use-spinner-progress.mjs +113 -0
- package/src/tui/dist/index.mjs +15540 -14452
- package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
- package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
- package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
- package/src/tui/session/agent-job-feed.mjs +4 -0
- package/src/tui/session/context-state.mjs +13 -1
- package/src/tui/session/initial-state.mjs +6 -0
- package/src/tui/session/quota-resume.mjs +263 -0
- package/src/tui/session/session-api/intake/abort.mjs +39 -35
- package/src/tui/session/session-api/intake/submission.mjs +13 -21
- package/src/tui/session/session-api/intake.mjs +3 -3
- package/src/tui/session/session-api/integrations.mjs +5 -1
- package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
- package/src/tui/session/session-api/lifecycle.mjs +71 -2
- package/src/tui/session/session-api/media.mjs +2 -2
- package/src/tui/session/session-api/quota-resume.mjs +21 -0
- package/src/tui/session/session-api/routes.mjs +2 -0
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +14 -6
- package/src/tui/session/session-flow/drain.mjs +52 -5
- package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
- package/src/tui/session/session-flow/queue.mjs +3 -0
- package/src/tui/session/session-flow/steering.mjs +11 -0
- package/src/tui/session/session-flow.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +7 -2
- package/src/tui/session/transcript-intake.mjs +1 -0
- package/src/tui/session/turn.mjs +40 -3
- package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
|
@@ -632,7 +632,7 @@ async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
|
|
|
632
632
|
};
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
-
export async function beginOAuthLogin({ openBrowserFn = null } = {}) {
|
|
635
|
+
export async function beginOAuthLogin({ openBrowserFn = null, openBrowser = true } = {}) {
|
|
636
636
|
const pkce = createOAuthPkce();
|
|
637
637
|
const state = randomBytes(32).toString('base64url');
|
|
638
638
|
const buildUrl = (redirectUri) => {
|
|
@@ -650,6 +650,7 @@ export async function beginOAuthLogin({ openBrowserFn = null } = {}) {
|
|
|
650
650
|
const url = buildUrl(OAUTH_REDIRECT_URI);
|
|
651
651
|
const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
|
|
652
652
|
const openLoginUrl = async (targetUrl, label = 'login') => {
|
|
653
|
+
if (!openBrowser) return;
|
|
653
654
|
try {
|
|
654
655
|
const openInBrowser = openBrowserFn || (await import('../../../shared/open-url.mjs')).openInBrowser;
|
|
655
656
|
await openInBrowser(targetUrl.toString());
|
|
@@ -7,6 +7,7 @@ import { createAbortController } from '../../../shared/abort-controller.mjs';
|
|
|
7
7
|
import { ANTHROPIC_MAX_MIDSTREAM_RETRIES, parseSSEStream } from './anthropic-sse.mjs';
|
|
8
8
|
import { buildAnthropicBetaHeaders } from './anthropic-betas.mjs';
|
|
9
9
|
import { buildAnthropicApiRequest } from './anthropic-api-request.mjs';
|
|
10
|
+
import { isCustomProviderName } from './custom-reasoning.mjs';
|
|
10
11
|
import { armFirstByteWatchdog, createAnthropicApiTransport } from './anthropic-api-transport.mjs';
|
|
11
12
|
import { createAnthropicApiRecovery } from './anthropic-api-recovery.mjs';
|
|
12
13
|
import { buildAnthropicTurnResult } from './anthropic-turn-result.mjs';
|
|
@@ -47,12 +48,18 @@ export class AnthropicProvider {
|
|
|
47
48
|
// off the client defaults and add it only on turns that actually serialize
|
|
48
49
|
// defer_loading tools.
|
|
49
50
|
_createClient(apiKey) {
|
|
50
|
-
|
|
51
|
+
// A custom gateway gets no first-party default betas and only its own
|
|
52
|
+
// credential: never the SDK's env/credential-file fallbacks, and none at
|
|
53
|
+
// all when keyless (config.keyless).
|
|
54
|
+
const custom = isCustomProviderName(this.name);
|
|
55
|
+
const keyless = custom && this.config.keyless === true;
|
|
56
|
+
const betaHeaders = this.config.disableBetaHeaders || custom ? null : buildAnthropicBetaHeaders();
|
|
51
57
|
return new (loadAnthropic())({
|
|
52
|
-
apiKey,
|
|
58
|
+
...(custom ? { apiKey: keyless ? '' : apiKey || '', authToken: null } : { apiKey }),
|
|
53
59
|
...(this.config.baseURL ? { baseURL: this.config.baseURL } : {}),
|
|
54
60
|
defaultHeaders: {
|
|
55
61
|
...(betaHeaders ? { 'anthropic-beta': betaHeaders } : {}),
|
|
62
|
+
...(keyless ? { 'x-api-key': null, authorization: null } : {}),
|
|
56
63
|
...(this.config.extraHeaders || {}),
|
|
57
64
|
},
|
|
58
65
|
maxRetries: 0,
|
|
@@ -114,6 +121,8 @@ export class AnthropicProvider {
|
|
|
114
121
|
tools,
|
|
115
122
|
opts,
|
|
116
123
|
fastModeLatched: this.fastModeBetaHeaderLatched,
|
|
124
|
+
// Custom providers carry per-model reasoning settings on their own rows.
|
|
125
|
+
modelInfo: isCustomProviderName(this.name) ? this.getCachedModelInfo?.(useModel) || null : null,
|
|
117
126
|
});
|
|
118
127
|
this.fastModeBetaHeaderLatched = fastModeLatched;
|
|
119
128
|
|
|
@@ -287,6 +287,7 @@ export async function beginOAuthLogin({
|
|
|
287
287
|
onProgress = null,
|
|
288
288
|
createServerFn = createServer,
|
|
289
289
|
openBrowserFn = null,
|
|
290
|
+
openBrowser = true,
|
|
290
291
|
} = {}) {
|
|
291
292
|
const controller = new AbortController();
|
|
292
293
|
const pkce = createOAuthPkce();
|
|
@@ -398,6 +399,7 @@ export async function beginOAuthLogin({
|
|
|
398
399
|
server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
|
|
399
400
|
if (settled) return;
|
|
400
401
|
process.stderr.write(`\n[antigravity-oauth] Open this URL to log in:\n${url.toString()}\n\n`);
|
|
402
|
+
if (!openBrowser) return;
|
|
401
403
|
try {
|
|
402
404
|
if (openBrowserFn) await openBrowserFn(url.toString());
|
|
403
405
|
else {
|
|
@@ -242,13 +242,13 @@ async function pollCursorOAuth(
|
|
|
242
242
|
throw new Error('Cursor OAuth timed out');
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
export async function beginCursorOAuthLogin(
|
|
245
|
+
export async function beginCursorOAuthLogin({ openBrowser = true, ...pollOptions } = {}) {
|
|
246
246
|
const params = generateCursorOAuthParams();
|
|
247
247
|
const controller = new AbortController();
|
|
248
248
|
process.stderr.write(`\n[cursor-oauth] Open the Cursor sign-in page:\n${params.loginUrl}\n\n`);
|
|
249
|
-
openInBrowser(params.loginUrl);
|
|
249
|
+
if (openBrowser) openInBrowser(params.loginUrl);
|
|
250
250
|
const waitForCallback = pollCursorOAuth(params.uuid, params.verifier, {
|
|
251
|
-
...
|
|
251
|
+
...pollOptions,
|
|
252
252
|
signal: controller.signal,
|
|
253
253
|
}).then((tokens) => {
|
|
254
254
|
saveCredentials(tokens);
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Custom provider config (schema v2): normalization, legacy migration,
|
|
2
|
+
// validation and the pure layering of per-model overrides on top of the
|
|
3
|
+
// auto-discovered (catalog-enriched) model list. No I/O.
|
|
4
|
+
|
|
5
|
+
import { CUSTOM_EFFORT_LEVELS, normalizeCustomEffort } from './custom-reasoning.mjs';
|
|
6
|
+
import { assertSafeBaseURL } from './provider-base-url.mjs';
|
|
7
|
+
|
|
8
|
+
export const CUSTOM_PROVIDER_PROTOCOLS = Object.freeze(['openai-chat', 'openai-responses', 'anthropic']);
|
|
9
|
+
export const CUSTOM_PROVIDER_SCHEMA_VERSION = 2;
|
|
10
|
+
// Ranked weakest → strongest.
|
|
11
|
+
export const CUSTOM_REASONING_LEVELS = CUSTOM_EFFORT_LEVELS;
|
|
12
|
+
export const CUSTOM_REASONING_WIRE_FORMATS = Object.freeze(['standard', 'deepseek']);
|
|
13
|
+
export const CUSTOM_PROVIDER_AUTH_MODES = Object.freeze(['key', 'none']);
|
|
14
|
+
export const CUSTOM_MODEL_OVERRIDE_LIMIT = 500;
|
|
15
|
+
|
|
16
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
17
|
+
const MAX_MODEL_ID = 256;
|
|
18
|
+
const MAX_NAME = 200;
|
|
19
|
+
const MAX_WIRE_VALUE = 64;
|
|
20
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point.
|
|
21
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
22
|
+
|
|
23
|
+
function fail(message) {
|
|
24
|
+
throw new Error(`[custom-provider] ${message}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function text(value) {
|
|
28
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function boundedText(value, label, max) {
|
|
36
|
+
if (typeof value !== 'string') fail(`${label} must be a string`);
|
|
37
|
+
const out = value.trim();
|
|
38
|
+
if (out.length > max) fail(`${label} must be at most ${max} characters`);
|
|
39
|
+
if (CONTROL_CHARS.test(out)) fail(`${label} must not contain control characters`);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Legacy (unversioned) input stays tolerant: blank → unset, numeric strings ok.
|
|
44
|
+
// An integer beyond the safe range is dropped (unset), so migration only ever
|
|
45
|
+
// emits values the v2 loader accepts.
|
|
46
|
+
function lenientPositiveInt(value, label) {
|
|
47
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
48
|
+
const n = Number(value);
|
|
49
|
+
if (!Number.isInteger(n) || n <= 0) fail(`${label} must be a positive integer`);
|
|
50
|
+
return Number.isSafeInteger(n) ? n : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// v2: a JSON number that is a safe positive integer, or absent.
|
|
54
|
+
function strictPositiveInt(value, label) {
|
|
55
|
+
if (value === undefined) return undefined;
|
|
56
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0)
|
|
57
|
+
fail(`${label} must be a positive integer`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const OVERRIDE_KEYS = new Set(['manual', 'hidden', 'name', 'contextWindow', 'maxOutputTokens', 'reasoning']);
|
|
62
|
+
const REASONING_KEYS = new Set(['levels', 'wireFormat', 'wireValues', 'replay']);
|
|
63
|
+
const DISCOVERY_KEYS = new Set(['newModelsHidden']);
|
|
64
|
+
// v2 config fields, plus the transport fields callers pass alongside them and
|
|
65
|
+
// the normalizer ignores: `id` / `apiKey` (setup input, validated by the
|
|
66
|
+
// service/IPC) and `preconnect` (runtime construction flag).
|
|
67
|
+
const V2_TOP_LEVEL_KEYS = new Set([
|
|
68
|
+
'type',
|
|
69
|
+
'schemaVersion',
|
|
70
|
+
'name',
|
|
71
|
+
'protocol',
|
|
72
|
+
'baseURL',
|
|
73
|
+
'enabled',
|
|
74
|
+
'auth',
|
|
75
|
+
'discovery',
|
|
76
|
+
'modelOverrides',
|
|
77
|
+
'id',
|
|
78
|
+
'apiKey',
|
|
79
|
+
'preconnect',
|
|
80
|
+
]);
|
|
81
|
+
const MAX_BASE_URL = 2048;
|
|
82
|
+
|
|
83
|
+
function onlyKnownKeys(value, allowed, label) {
|
|
84
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) fail(`${label} has unsupported field: ${key}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optionalBoolean(value, label) {
|
|
88
|
+
if (value === undefined) return undefined;
|
|
89
|
+
if (typeof value !== 'boolean') fail(`${label} must be a boolean`);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeBaseURL(raw) {
|
|
94
|
+
const value = text(raw);
|
|
95
|
+
if (!value) fail('baseURL is required');
|
|
96
|
+
if (value.length > MAX_BASE_URL) fail(`baseURL must be at most ${MAX_BASE_URL} characters`);
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(value);
|
|
100
|
+
} catch {
|
|
101
|
+
fail(`invalid baseURL: ${value}`);
|
|
102
|
+
}
|
|
103
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') fail('baseURL must use http or https');
|
|
104
|
+
if (url.username || url.password) fail('baseURL must not embed credentials');
|
|
105
|
+
if (url.search || url.hash || /[?#]/.test(value)) fail('baseURL must not contain a query or hash');
|
|
106
|
+
return assertSafeBaseURL(value.replace(/\/+$/, ''), 'custom');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function modelIdKey(raw, label = 'model id') {
|
|
110
|
+
const id = typeof raw === 'string' ? raw : '';
|
|
111
|
+
if (!id.trim()) fail(`every ${label} must be a non-empty string`);
|
|
112
|
+
if (id !== id.trim()) fail(`${label} must not have surrounding whitespace: ${JSON.stringify(id)}`);
|
|
113
|
+
if (id.length > MAX_MODEL_ID) fail(`${label} must be at most ${MAX_MODEL_ID} characters`);
|
|
114
|
+
if (CONTROL_CHARS.test(id)) fail(`${label} must not contain control characters`);
|
|
115
|
+
if (UNSAFE_KEYS.has(id)) fail(`unsafe ${label}: ${id}`);
|
|
116
|
+
return id;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Canonical level for `raw` (alias-aware), or null when unknown. */
|
|
120
|
+
export function canonicalReasoningLevel(raw) {
|
|
121
|
+
return typeof raw === 'string' ? normalizeCustomEffort(raw) : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rank = (level) => CUSTOM_REASONING_LEVELS.indexOf(level);
|
|
125
|
+
const byRank = (a, b) => rank(a) - rank(b);
|
|
126
|
+
|
|
127
|
+
function normalizeLevels(raw, label) {
|
|
128
|
+
if (!Array.isArray(raw)) fail(`${label} must be an array`);
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
for (const entry of raw) {
|
|
131
|
+
const level = canonicalReasoningLevel(entry);
|
|
132
|
+
if (!level) fail(`${label} has unknown level: ${String(entry)}`);
|
|
133
|
+
if (seen.has(level)) fail(`${label} has duplicate level: ${level}`);
|
|
134
|
+
seen.add(level);
|
|
135
|
+
}
|
|
136
|
+
return [...seen].sort(byRank);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeReasoning(raw, label, protocol) {
|
|
140
|
+
if (!isPlainObject(raw)) fail(`${label} must be an object`);
|
|
141
|
+
onlyKnownKeys(raw, REASONING_KEYS, label);
|
|
142
|
+
const out = {};
|
|
143
|
+
if (raw.levels !== undefined) out.levels = normalizeLevels(raw.levels, `${label}.levels`);
|
|
144
|
+
if (raw.wireFormat !== undefined) {
|
|
145
|
+
const wireFormat = text(raw.wireFormat);
|
|
146
|
+
if (!CUSTOM_REASONING_WIRE_FORMATS.includes(wireFormat)) fail(`${label}.wireFormat is invalid: ${raw.wireFormat}`);
|
|
147
|
+
if (wireFormat === 'deepseek' && protocol !== 'openai-chat')
|
|
148
|
+
fail(`${label}.wireFormat 'deepseek' requires protocol 'openai-chat'`);
|
|
149
|
+
out.wireFormat = wireFormat;
|
|
150
|
+
}
|
|
151
|
+
if (raw.wireValues !== undefined) out.wireValues = normalizeWireValues(raw.wireValues, label, out.levels);
|
|
152
|
+
const replay = optionalBoolean(raw.replay, `${label}.replay`);
|
|
153
|
+
if (replay !== undefined) out.replay = replay;
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// `levels` (canonical) ties every key to a declared level; null skips the tie.
|
|
158
|
+
function normalizeWireValues(raw, label, levels) {
|
|
159
|
+
if (!isPlainObject(raw)) fail(`${label}.wireValues must be an object`);
|
|
160
|
+
const wireValues = {};
|
|
161
|
+
for (const key of Object.keys(raw)) {
|
|
162
|
+
const level = canonicalReasoningLevel(key);
|
|
163
|
+
if (!level) fail(`${label}.wireValues has unknown level: ${key}`);
|
|
164
|
+
if (levels && !levels.includes(level)) fail(`${label}.wireValues level ${level} is not in levels`);
|
|
165
|
+
if (level in wireValues) fail(`${label}.wireValues has duplicate level: ${level}`);
|
|
166
|
+
const value = boundedText(raw[key], `${label}.wireValues.${level}`, MAX_WIRE_VALUE);
|
|
167
|
+
if (!value) fail(`${label}.wireValues.${level} must be non-empty`);
|
|
168
|
+
wireValues[level] = value;
|
|
169
|
+
}
|
|
170
|
+
return Object.fromEntries(Object.entries(wireValues).sort(([a], [b]) => byRank(a, b)));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Shape check for a tri-state reasoning patch (null deletes a field): the
|
|
175
|
+
* stored-override rules for every supplied value, minus the levels ↔
|
|
176
|
+
* wireValues tie, which the caller settles after merging (pruning).
|
|
177
|
+
*/
|
|
178
|
+
export function assertCustomReasoningPatch(raw) {
|
|
179
|
+
const label = 'reasoning patch';
|
|
180
|
+
if (!isPlainObject(raw)) fail(`${label} must be an object`);
|
|
181
|
+
onlyKnownKeys(raw, REASONING_KEYS, label);
|
|
182
|
+
const supplied = (key) => raw[key] !== undefined && raw[key] !== null;
|
|
183
|
+
if (supplied('levels')) normalizeLevels(raw.levels, `${label}.levels`);
|
|
184
|
+
if (supplied('wireFormat') && !CUSTOM_REASONING_WIRE_FORMATS.includes(text(raw.wireFormat)))
|
|
185
|
+
fail(`${label}.wireFormat is invalid: ${String(raw.wireFormat)}`);
|
|
186
|
+
if (supplied('wireValues')) normalizeWireValues(raw.wireValues, label, null);
|
|
187
|
+
if (supplied('replay')) optionalBoolean(raw.replay, `${label}.replay`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `legacy` marks overrides generated by migrateLegacy from an unversioned
|
|
191
|
+
// config, whose tolerant value rules are kept; everything else is v2-strict.
|
|
192
|
+
function normalizeOverride(id, raw, protocol, legacy) {
|
|
193
|
+
const label = `modelOverrides[${id}]`;
|
|
194
|
+
if (!isPlainObject(raw)) fail(`${label} must be an object`);
|
|
195
|
+
onlyKnownKeys(raw, OVERRIDE_KEYS, label);
|
|
196
|
+
const positiveInt = legacy ? lenientPositiveInt : strictPositiveInt;
|
|
197
|
+
const out = {};
|
|
198
|
+
const manual = optionalBoolean(raw.manual, `${label}.manual`);
|
|
199
|
+
if (manual) out.manual = true;
|
|
200
|
+
const hidden = optionalBoolean(raw.hidden, `${label}.hidden`);
|
|
201
|
+
if (hidden !== undefined) out.hidden = hidden;
|
|
202
|
+
if (raw.name !== undefined) {
|
|
203
|
+
const name = boundedText(raw.name, `${label}.name`, MAX_NAME);
|
|
204
|
+
if (!name && !legacy) fail(`${label}.name must be non-empty`);
|
|
205
|
+
if (name) out.name = name;
|
|
206
|
+
}
|
|
207
|
+
const contextWindow = positiveInt(raw.contextWindow, `${label}.contextWindow`);
|
|
208
|
+
if (contextWindow) out.contextWindow = contextWindow;
|
|
209
|
+
const maxOutputTokens = positiveInt(raw.maxOutputTokens, `${label}.maxOutputTokens`);
|
|
210
|
+
if (maxOutputTokens) out.maxOutputTokens = maxOutputTokens;
|
|
211
|
+
if (raw.reasoning !== undefined) {
|
|
212
|
+
const reasoning = normalizeReasoning(raw.reasoning, `${label}.reasoning`, protocol);
|
|
213
|
+
if (Object.keys(reasoning).length) out.reasoning = reasoning;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeOverrides(raw, protocol, legacy) {
|
|
219
|
+
if (raw === undefined) return {};
|
|
220
|
+
if (!isPlainObject(raw)) fail('modelOverrides must be an object');
|
|
221
|
+
const keys = Object.keys(raw);
|
|
222
|
+
if (keys.length > CUSTOM_MODEL_OVERRIDE_LIMIT)
|
|
223
|
+
fail(`at most ${CUSTOM_MODEL_OVERRIDE_LIMIT} model overrides are allowed`);
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const key of keys) {
|
|
226
|
+
const id = modelIdKey(key, 'modelOverrides key');
|
|
227
|
+
out[id] = normalizeOverride(id, raw[key], protocol, legacy);
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Legacy (pre-v2) `models: [{ id, name?, contextWindow?, maxOutputTokens? }]`
|
|
233
|
+
// become manual, visible overrides; their presence hides newly discovered ids.
|
|
234
|
+
function migrateLegacy(input) {
|
|
235
|
+
const raw = input.models === undefined || input.models === null ? [] : input.models;
|
|
236
|
+
if (!Array.isArray(raw)) fail('models must be an array');
|
|
237
|
+
if (raw.length > CUSTOM_MODEL_OVERRIDE_LIMIT) fail(`at most ${CUSTOM_MODEL_OVERRIDE_LIMIT} models are allowed`);
|
|
238
|
+
const modelOverrides = {};
|
|
239
|
+
for (const entry of raw) {
|
|
240
|
+
const id = modelIdKey(text(entry?.id), 'model id');
|
|
241
|
+
if (Object.hasOwn(modelOverrides, id)) fail(`duplicate model id: ${id}`);
|
|
242
|
+
const override = { manual: true, hidden: false };
|
|
243
|
+
const name = text(entry.name);
|
|
244
|
+
if (name) override.name = name;
|
|
245
|
+
if (entry.contextWindow !== undefined) override.contextWindow = entry.contextWindow;
|
|
246
|
+
if (entry.maxOutputTokens !== undefined) override.maxOutputTokens = entry.maxOutputTokens;
|
|
247
|
+
modelOverrides[id] = override;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
...input,
|
|
251
|
+
// Legacy entries predate keyless auth; a caller-stated auth still applies.
|
|
252
|
+
auth: input.auth ?? 'key',
|
|
253
|
+
discovery: { newModelsHidden: raw.length > 0 },
|
|
254
|
+
modelOverrides,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Normalized, secret-free v2 config. Legacy (no schemaVersion) input is
|
|
260
|
+
* migrated deterministically; unknown future versions are rejected.
|
|
261
|
+
*/
|
|
262
|
+
export function normalizeCustomProviderConfig(input) {
|
|
263
|
+
if (!isPlainObject(input)) fail('config must be an object');
|
|
264
|
+
let source = input;
|
|
265
|
+
const version = input.schemaVersion;
|
|
266
|
+
const legacy = version === undefined || version === null;
|
|
267
|
+
if (legacy) {
|
|
268
|
+
// v2 fields without the version would be silently dropped by migration.
|
|
269
|
+
if (input.modelOverrides !== undefined || input.discovery !== undefined)
|
|
270
|
+
fail('modelOverrides and discovery require schemaVersion 2');
|
|
271
|
+
source = migrateLegacy(input);
|
|
272
|
+
} else if (Number.isInteger(version) && version > CUSTOM_PROVIDER_SCHEMA_VERSION)
|
|
273
|
+
fail(`unsupported schemaVersion ${version}; update mixdog to use this provider`);
|
|
274
|
+
else if (version !== CUSTOM_PROVIDER_SCHEMA_VERSION) fail(`invalid schemaVersion: ${String(version)}`);
|
|
275
|
+
if (!legacy) {
|
|
276
|
+
// v2 top level is strict (same accept set as the desktop IPC validator):
|
|
277
|
+
// no unknown fields, exact enum strings, boolean `enabled`. An omitted
|
|
278
|
+
// `auth` means 'key' here; edits keep the stored value in the service.
|
|
279
|
+
onlyKnownKeys(input, V2_TOP_LEVEL_KEYS, 'config');
|
|
280
|
+
if (input.type !== undefined && input.type !== 'custom') fail(`invalid type: ${String(input.type)}`);
|
|
281
|
+
if (input.protocol !== undefined && typeof input.protocol !== 'string') fail('protocol must be a string');
|
|
282
|
+
if (typeof input.protocol === 'string' && input.protocol !== input.protocol.trim())
|
|
283
|
+
fail(`unsupported protocol: ${input.protocol}`);
|
|
284
|
+
if (input.auth !== undefined && !CUSTOM_PROVIDER_AUTH_MODES.includes(input.auth))
|
|
285
|
+
fail(`invalid auth: ${String(input.auth)}`);
|
|
286
|
+
optionalBoolean(input.enabled, 'enabled');
|
|
287
|
+
optionalBoolean(input.preconnect, 'preconnect');
|
|
288
|
+
}
|
|
289
|
+
const name = boundedText(source.name ?? '', 'name', MAX_NAME);
|
|
290
|
+
if (!name) fail('name is required');
|
|
291
|
+
const protocol = text(source.protocol);
|
|
292
|
+
if (!protocol) fail('protocol is required');
|
|
293
|
+
if (!CUSTOM_PROVIDER_PROTOCOLS.includes(protocol)) fail(`unsupported protocol: ${protocol}`);
|
|
294
|
+
const auth = source.auth === undefined || source.auth === null ? 'key' : text(source.auth);
|
|
295
|
+
if (!CUSTOM_PROVIDER_AUTH_MODES.includes(auth)) fail(`invalid auth: ${String(source.auth)}`);
|
|
296
|
+
const discovery = source.discovery === undefined ? {} : source.discovery;
|
|
297
|
+
if (!isPlainObject(discovery)) fail('discovery must be an object');
|
|
298
|
+
onlyKnownKeys(discovery, DISCOVERY_KEYS, 'discovery');
|
|
299
|
+
const newModelsHidden = optionalBoolean(discovery.newModelsHidden, 'discovery.newModelsHidden') ?? false;
|
|
300
|
+
return {
|
|
301
|
+
type: 'custom',
|
|
302
|
+
schemaVersion: CUSTOM_PROVIDER_SCHEMA_VERSION,
|
|
303
|
+
name,
|
|
304
|
+
protocol,
|
|
305
|
+
baseURL: normalizeBaseURL(source.baseURL),
|
|
306
|
+
enabled: source.enabled !== false,
|
|
307
|
+
auth,
|
|
308
|
+
discovery: { newModelsHidden },
|
|
309
|
+
modelOverrides: normalizeOverrides(source.modelOverrides, protocol, legacy),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function catalogLevels(row) {
|
|
314
|
+
const options = Array.isArray(row?.reasoningOptions) ? row.reasoningOptions : [];
|
|
315
|
+
const levels = new Set();
|
|
316
|
+
for (const option of options) {
|
|
317
|
+
if (option?.type !== 'effort' || !Array.isArray(option.values)) continue;
|
|
318
|
+
for (const value of option.values) {
|
|
319
|
+
const level = canonicalReasoningLevel(value);
|
|
320
|
+
if (level) levels.add(level);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return levels.size ? [...levels].sort(byRank) : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function effectiveRow(base, override, { providerId, discovered, newModelsHidden }) {
|
|
327
|
+
const ov = override || {};
|
|
328
|
+
const reasoning = ov.reasoning || {};
|
|
329
|
+
const {
|
|
330
|
+
reasoningOptions: _options,
|
|
331
|
+
reasoningLevels: _levels,
|
|
332
|
+
reasoningContentField,
|
|
333
|
+
customReasoning: _custom,
|
|
334
|
+
...rest
|
|
335
|
+
} = base;
|
|
336
|
+
const levels = reasoning.levels ?? catalogLevels(base);
|
|
337
|
+
const replay = reasoning.replay ?? reasoningContentField === 'reasoning_content';
|
|
338
|
+
const wireValues = {};
|
|
339
|
+
for (const [level, value] of Object.entries(reasoning.wireValues || {})) {
|
|
340
|
+
if (!levels || levels.includes(level)) wireValues[level] = value;
|
|
341
|
+
}
|
|
342
|
+
const hidden = 'hidden' in ov ? ov.hidden : discovered && !override && newModelsHidden;
|
|
343
|
+
return {
|
|
344
|
+
...rest,
|
|
345
|
+
id: base.id,
|
|
346
|
+
name: ov.name ?? (base.name || base.id),
|
|
347
|
+
provider: providerId || base.provider,
|
|
348
|
+
contextWindow: ov.contextWindow ?? (Number(base.contextWindow) > 0 ? Number(base.contextWindow) : 0),
|
|
349
|
+
outputTokens: ov.maxOutputTokens ?? (Number(base.outputTokens) > 0 ? Number(base.outputTokens) : null),
|
|
350
|
+
hidden,
|
|
351
|
+
manual: ov.manual === true,
|
|
352
|
+
supportsReasoning: levels ? levels.some((level) => level !== 'none') : base.supportsReasoning === true,
|
|
353
|
+
...(levels
|
|
354
|
+
? {
|
|
355
|
+
reasoningOptions: levels.length ? [{ type: 'effort', values: [...levels] }] : [],
|
|
356
|
+
reasoningLevels: [...levels],
|
|
357
|
+
}
|
|
358
|
+
: {}),
|
|
359
|
+
customReasoning: { wireFormat: reasoning.wireFormat ?? 'standard', wireValues, replay },
|
|
360
|
+
...(replay ? { reasoningContentField: 'reasoning_content' } : {}),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Pure layering: discovered rows (catalog-enriched) as base → overrides by
|
|
366
|
+
* property presence → manual overrides missing from discovery appended.
|
|
367
|
+
* `config` is a normalized v2 config. Hidden rows are kept.
|
|
368
|
+
*/
|
|
369
|
+
export function effectiveCustomModelRows(config, discoveredRows, { providerId } = {}) {
|
|
370
|
+
const overrides = config?.modelOverrides || {};
|
|
371
|
+
const newModelsHidden = config?.discovery?.newModelsHidden === true;
|
|
372
|
+
const rows = [];
|
|
373
|
+
const seen = new Set();
|
|
374
|
+
for (const row of Array.isArray(discoveredRows) ? discoveredRows : []) {
|
|
375
|
+
const id = typeof row?.id === 'string' ? row.id : '';
|
|
376
|
+
if (!id || seen.has(id)) continue;
|
|
377
|
+
seen.add(id);
|
|
378
|
+
const override = Object.hasOwn(overrides, id) ? overrides[id] : undefined;
|
|
379
|
+
rows.push(effectiveRow(row, override, { providerId, discovered: true, newModelsHidden }));
|
|
380
|
+
}
|
|
381
|
+
for (const [id, override] of Object.entries(overrides)) {
|
|
382
|
+
if (!override?.manual || seen.has(id)) continue;
|
|
383
|
+
seen.add(id);
|
|
384
|
+
const base = { id, name: id, provider: providerId, contextWindow: 0, outputTokens: null };
|
|
385
|
+
rows.push(effectiveRow(base, override, { providerId, discovered: false, newModelsHidden }));
|
|
386
|
+
}
|
|
387
|
+
return rows;
|
|
388
|
+
}
|