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
|
@@ -4,120 +4,92 @@
|
|
|
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 { effectiveCustomModelRows, normalizeCustomProviderConfig } from './custom-provider-config.mjs';
|
|
7
8
|
import { ANTHROPIC_VERSION } from './lib/anthropic-models.mjs';
|
|
8
|
-
import {
|
|
9
|
-
import { listCompatModels } from './openai-compat-models.mjs';
|
|
9
|
+
import { enrichModels } from './model-catalog.mjs';
|
|
10
|
+
import { fetchCompatModelItems, listCompatModels } from './openai-compat-models.mjs';
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
+
export {
|
|
13
|
+
CUSTOM_PROVIDER_PROTOCOLS,
|
|
14
|
+
CUSTOM_PROVIDER_SCHEMA_VERSION,
|
|
15
|
+
CUSTOM_REASONING_LEVELS,
|
|
16
|
+
assertCustomReasoningPatch,
|
|
17
|
+
canonicalReasoningLevel,
|
|
18
|
+
effectiveCustomModelRows,
|
|
19
|
+
normalizeCustomProviderConfig,
|
|
20
|
+
} from './custom-provider-config.mjs';
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
throw new Error(`[custom-provider] ${message}`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function text(value) {
|
|
18
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function positiveInt(value, label) {
|
|
22
|
-
if (value === undefined || value === null || value === '') return undefined;
|
|
23
|
-
const n = Number(value);
|
|
24
|
-
if (!Number.isInteger(n) || n <= 0) fail(`${label} must be a positive integer`);
|
|
25
|
-
return n;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function normalizeBaseURL(raw) {
|
|
29
|
-
const value = text(raw);
|
|
30
|
-
if (!value) fail('baseURL is required');
|
|
31
|
-
let url;
|
|
32
|
-
try {
|
|
33
|
-
url = new URL(value);
|
|
34
|
-
} catch {
|
|
35
|
-
fail(`invalid baseURL: ${value}`);
|
|
36
|
-
}
|
|
37
|
-
if (url.protocol !== 'https:' && url.protocol !== 'http:') fail('baseURL must use http or https');
|
|
38
|
-
if (url.username || url.password) fail('baseURL must not embed credentials');
|
|
39
|
-
if (url.search || url.hash || /[?#]/.test(value)) fail('baseURL must not contain a query or hash');
|
|
40
|
-
return assertSafeBaseURL(value.replace(/\/+$/, ''), 'custom');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function normalizeModels(raw, requireModels) {
|
|
44
|
-
if (raw === undefined || raw === null) raw = [];
|
|
45
|
-
if (!Array.isArray(raw)) fail('models must be an array');
|
|
46
|
-
const seen = new Set();
|
|
47
|
-
const models = raw.map((entry) => {
|
|
48
|
-
const id = text(entry?.id);
|
|
49
|
-
if (!id) fail('every model needs an id');
|
|
50
|
-
if (seen.has(id)) fail(`duplicate model id: ${id}`);
|
|
51
|
-
seen.add(id);
|
|
52
|
-
const name = text(entry.name);
|
|
53
|
-
const contextWindow = positiveInt(entry.contextWindow, `model ${id} contextWindow`);
|
|
54
|
-
const maxOutputTokens = positiveInt(entry.maxOutputTokens, `model ${id} maxOutputTokens`);
|
|
55
|
-
return {
|
|
56
|
-
id,
|
|
57
|
-
...(name ? { name } : {}),
|
|
58
|
-
...(contextWindow ? { contextWindow } : {}),
|
|
59
|
-
...(maxOutputTokens ? { maxOutputTokens } : {}),
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
if (requireModels && models.length === 0) fail('at least one model is required');
|
|
63
|
-
return models;
|
|
64
|
-
}
|
|
22
|
+
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
65
23
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
type: 'custom',
|
|
75
|
-
name,
|
|
76
|
-
protocol,
|
|
77
|
-
baseURL: normalizeBaseURL(input.baseURL),
|
|
78
|
-
models: normalizeModels(input.models, requireModels),
|
|
79
|
-
enabled: true,
|
|
80
|
-
};
|
|
24
|
+
// Discovery error safe to surface: the key is redacted, the HTTP status kept.
|
|
25
|
+
function sanitizeDiscoveryError(error, apiKey) {
|
|
26
|
+
let message = String(error?.message || error || 'model discovery failed');
|
|
27
|
+
if (apiKey) message = message.split(apiKey).join('[redacted]');
|
|
28
|
+
const out = new Error(message);
|
|
29
|
+
if (Number.isInteger(error?.status)) out.status = error.status;
|
|
30
|
+
return out;
|
|
81
31
|
}
|
|
82
32
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
contextWindow: m.contextWindow || 0,
|
|
89
|
-
outputTokens: m.maxOutputTokens || null,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Configured models win; an empty list falls back to protocol discovery.
|
|
94
|
-
function withConfiguredModels(Base, discover) {
|
|
33
|
+
// Discovery always runs; overrides layer on top. A failed discovery keeps the
|
|
34
|
+
// last successful discovered rows plus manual rows, and throws only when
|
|
35
|
+
// nothing is usable or the caller asks (`throwOnError`). The sanitized error
|
|
36
|
+
// of the latest attempt is kept in `customDiscoveryError` (null on success).
|
|
37
|
+
function withCustomModels(Base, discover) {
|
|
95
38
|
return class extends Base {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
39
|
+
customConfig = null;
|
|
40
|
+
customDiscoveryError = null;
|
|
41
|
+
_customDiscovered = null;
|
|
42
|
+
_customApiKey = '';
|
|
43
|
+
_customKeyless = false;
|
|
44
|
+
_customRows() {
|
|
45
|
+
return effectiveCustomModelRows(this.customConfig, this._customDiscovered || [], { providerId: this.name });
|
|
46
|
+
}
|
|
47
|
+
_applyCustomRows(rows) {
|
|
48
|
+
this._enrichedModels = rows;
|
|
49
|
+
if (rows.some((row) => row.id === this.defaultModel && !row.hidden)) return;
|
|
50
|
+
const first = rows.find((row) => !row.hidden);
|
|
51
|
+
if (first) this.defaultModel = first.id;
|
|
52
|
+
}
|
|
53
|
+
async listModels({ signal = null, throwOnError = false } = {}) {
|
|
54
|
+
let failure = null;
|
|
55
|
+
try {
|
|
56
|
+
this._customDiscovered = await discover(this, signal);
|
|
57
|
+
this.customDiscoveryError = null;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (signal?.aborted) throw error;
|
|
60
|
+
failure = sanitizeDiscoveryError(error, this._customApiKey);
|
|
61
|
+
this.customDiscoveryError = failure;
|
|
62
|
+
}
|
|
63
|
+
const rows = this._customRows();
|
|
64
|
+
this._applyCustomRows(rows);
|
|
65
|
+
if (failure && (throwOnError || rows.length === 0)) throw failure;
|
|
66
|
+
return rows.map((row) => structuredClone(row));
|
|
101
67
|
}
|
|
102
68
|
async isAvailable() {
|
|
103
|
-
|
|
69
|
+
if (this._enrichedModels?.length) return true;
|
|
70
|
+
if (this._customKeyless && this.customConfig.protocol === 'anthropic') return true;
|
|
71
|
+
return super.isAvailable();
|
|
104
72
|
}
|
|
105
73
|
getCachedModelInfo(model) {
|
|
106
|
-
return this.
|
|
74
|
+
return this._enrichedModels?.find((row) => row.id === model) || null;
|
|
107
75
|
}
|
|
108
76
|
};
|
|
109
77
|
}
|
|
110
78
|
|
|
111
|
-
async function discoverAnthropicModels(provider) {
|
|
79
|
+
async function discoverAnthropicModels(provider, signal) {
|
|
80
|
+
const timeout = AbortSignal.timeout(DISCOVERY_TIMEOUT_MS);
|
|
112
81
|
const res = await fetch(`${provider.config.baseURL}/v1/models`, {
|
|
113
82
|
method: 'GET',
|
|
114
|
-
signal: AbortSignal.timeout
|
|
115
|
-
headers: {
|
|
83
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
84
|
+
headers: {
|
|
85
|
+
...(provider._customKeyless ? {} : { 'x-api-key': provider.apiKey || '' }),
|
|
86
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
87
|
+
},
|
|
116
88
|
});
|
|
117
89
|
if (!res.ok) throw Object.assign(new Error(`${provider.name} models ${res.status}`), { status: res.status });
|
|
118
90
|
const data = await res.json();
|
|
119
91
|
const items = Array.isArray(data?.data) ? data.data : [];
|
|
120
|
-
|
|
92
|
+
const rows = items
|
|
121
93
|
.filter((m) => m?.id)
|
|
122
94
|
.map((m) => ({
|
|
123
95
|
id: m.id,
|
|
@@ -126,40 +98,51 @@ async function discoverAnthropicModels(provider) {
|
|
|
126
98
|
contextWindow: Number(m.max_input_tokens) > 0 ? Number(m.max_input_tokens) : 0,
|
|
127
99
|
outputTokens: Number(m.max_tokens) > 0 ? Number(m.max_tokens) : null,
|
|
128
100
|
}));
|
|
101
|
+
return enrichModels(rows);
|
|
129
102
|
}
|
|
130
103
|
|
|
131
104
|
export async function createCustomProvider(id, config) {
|
|
132
|
-
const providerId =
|
|
133
|
-
if (!/^custom-.+/.test(providerId))
|
|
134
|
-
const cfg = normalizeCustomProviderConfig(config
|
|
105
|
+
const providerId = typeof id === 'string' ? id.trim() : '';
|
|
106
|
+
if (!/^custom-.+/.test(providerId)) throw new Error(`[custom-provider] invalid custom provider id: ${id}`);
|
|
107
|
+
const cfg = normalizeCustomProviderConfig(config);
|
|
108
|
+
// `auth: 'none'` is authoritative: a supplied/injected key is ignored and
|
|
109
|
+
// `keyless` makes the engine omit every credential header.
|
|
110
|
+
const keyless = cfg.auth === 'none';
|
|
111
|
+
const apiKey = keyless ? '' : config.apiKey || '';
|
|
135
112
|
const engine = {
|
|
136
113
|
baseURL: cfg.baseURL,
|
|
137
|
-
apiKey:
|
|
114
|
+
apiKey: apiKey || undefined,
|
|
115
|
+
...(keyless ? { keyless: true } : {}),
|
|
138
116
|
...(config.preconnect === false ? { preconnect: false } : {}),
|
|
139
117
|
};
|
|
140
118
|
let provider;
|
|
141
119
|
if (cfg.protocol === 'anthropic') {
|
|
142
120
|
const { AnthropicProvider } = await import('./anthropic.mjs');
|
|
143
|
-
const Custom =
|
|
121
|
+
const Custom = withCustomModels(AnthropicProvider, discoverAnthropicModels);
|
|
144
122
|
// The Anthropic SDK appends /v1/messages itself; accept a /v1 base URL.
|
|
145
123
|
provider = new Custom({ ...engine, name: providerId, baseURL: cfg.baseURL.replace(/\/v1$/i, '') });
|
|
146
124
|
// An unknown third-party gateway behind the Anthropic wire: PDFs lower to text.
|
|
147
125
|
provider.nativePdf = false;
|
|
148
126
|
} else {
|
|
149
127
|
const { OpenAICompatProvider } = await import('./openai-compat.mjs');
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
:
|
|
158
|
-
|
|
128
|
+
const responses = cfg.protocol === 'openai-responses';
|
|
129
|
+
class CompatBase extends OpenAICompatProvider {
|
|
130
|
+
_fetchModelItems({ signal = null } = {}) {
|
|
131
|
+
return fetchCompatModelItems(this, { signal, keyless: this._customKeyless });
|
|
132
|
+
}
|
|
133
|
+
_doSend(messages, model, tools, sendOpts) {
|
|
134
|
+
if (!responses) return super._doSend(messages, model, tools, sendOpts);
|
|
135
|
+
return super._doSend(messages, model, tools, { ...(sendOpts || {}), compatWireApi: 'responses' });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const Custom = withCustomModels(CompatBase, (self, signal) =>
|
|
139
|
+
listCompatModels(self, { throwOnError: true, signal })
|
|
140
|
+
);
|
|
159
141
|
provider = new Custom(providerId, engine);
|
|
160
|
-
if (cfg.models.length) provider.defaultModel = cfg.models[0].id;
|
|
161
142
|
}
|
|
162
|
-
provider._customModels = cfg.models.map((m) => modelRow(providerId, m));
|
|
163
143
|
provider.customConfig = cfg;
|
|
144
|
+
provider._customApiKey = apiKey;
|
|
145
|
+
provider._customKeyless = keyless;
|
|
146
|
+
provider._applyCustomRows(provider._customRows());
|
|
164
147
|
return provider;
|
|
165
148
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Per-model reasoning settings for user-defined (`custom-*`) providers. Pure:
|
|
2
|
+
// the effective model row (custom-provider getCachedModelInfo) carries the
|
|
3
|
+
// supported levels and the wire mapping; this module snaps the requested
|
|
4
|
+
// effort to a supported level and serializes it for the provider protocol.
|
|
5
|
+
// First-party providers never route through here.
|
|
6
|
+
|
|
7
|
+
export const CUSTOM_EFFORT_LEVELS = Object.freeze(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
|
|
8
|
+
const RANK = new Map(CUSTOM_EFFORT_LEVELS.map((level, index) => [level, index]));
|
|
9
|
+
|
|
10
|
+
export function isCustomProviderName(name) {
|
|
11
|
+
return String(name || '').startsWith('custom-');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One effort token in the custom vocabulary (`ultra` → `max`), or null. */
|
|
15
|
+
export function normalizeCustomEffort(value) {
|
|
16
|
+
const v = String(value ?? '')
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase();
|
|
19
|
+
const level = v === 'ultra' ? 'max' : v;
|
|
20
|
+
return RANK.has(level) ? level : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Supported levels declared on the row, ranked and deduplicated, or null when
|
|
25
|
+
* the row declares none (unknown). An explicit [] stays [] (no reasoning).
|
|
26
|
+
*/
|
|
27
|
+
export function customEffortLevels(model) {
|
|
28
|
+
const option = Array.isArray(model?.reasoningOptions)
|
|
29
|
+
? model.reasoningOptions.find(
|
|
30
|
+
(entry) =>
|
|
31
|
+
String(entry?.type || '')
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase() === 'effort'
|
|
34
|
+
)
|
|
35
|
+
: null;
|
|
36
|
+
let raw = null;
|
|
37
|
+
if (Array.isArray(option?.values)) raw = option.values;
|
|
38
|
+
else if (Array.isArray(model?.reasoningLevels)) raw = model.reasoningLevels;
|
|
39
|
+
if (!raw) return null;
|
|
40
|
+
const levels = new Set(raw.map(normalizeCustomEffort).filter(Boolean));
|
|
41
|
+
return CUSTOM_EFFORT_LEVELS.filter((level) => levels.has(level));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Snap a requested effort onto `levels`. Exact matches stay; `none` is kept
|
|
46
|
+
* only when supported; otherwise the strongest supported non-none level not
|
|
47
|
+
* stronger than the request. Never promotes; no eligible level → null.
|
|
48
|
+
*/
|
|
49
|
+
export function snapCustomEffort(levels, effort) {
|
|
50
|
+
const requested = normalizeCustomEffort(effort);
|
|
51
|
+
if (!requested || !Array.isArray(levels) || levels.length === 0) return null;
|
|
52
|
+
const supported = levels.map(normalizeCustomEffort).filter(Boolean);
|
|
53
|
+
if (supported.includes(requested)) return requested;
|
|
54
|
+
if (requested === 'none') return null;
|
|
55
|
+
const ceiling = RANK.get(requested);
|
|
56
|
+
let best = null;
|
|
57
|
+
for (const level of supported) {
|
|
58
|
+
const rank = RANK.get(level);
|
|
59
|
+
if (level === 'none' || rank === undefined || rank > ceiling) continue;
|
|
60
|
+
if (best === null || rank > RANK.get(best)) best = level;
|
|
61
|
+
}
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const EMPTY = Object.freeze({ level: null, fields: {}, thinking: null, replay: false });
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {{ protocol: 'openai-chat'|'openai-responses'|'anthropic', model: object|null, effort: string|null|undefined }} input
|
|
69
|
+
* @returns {{ level: string|null, fields: object, thinking: 'on'|'off'|null, replay: boolean }}
|
|
70
|
+
*/
|
|
71
|
+
export function resolveCustomReasoning({ protocol, model, effort } = {}) {
|
|
72
|
+
const custom = model?.customReasoning || {};
|
|
73
|
+
const wireFormat = custom.wireFormat === 'deepseek' ? 'deepseek' : 'standard';
|
|
74
|
+
// DeepSeek's `thinking` extension exists only on the Chat wire.
|
|
75
|
+
if (wireFormat === 'deepseek' && protocol !== 'openai-chat') return { ...EMPTY, fields: {} };
|
|
76
|
+
const level = snapCustomEffort(customEffortLevels(model), effort);
|
|
77
|
+
let thinking = null;
|
|
78
|
+
if (level !== null) thinking = level === 'none' ? 'off' : 'on';
|
|
79
|
+
const mapped = level === null ? null : custom.wireValues?.[level];
|
|
80
|
+
const value = typeof mapped === 'string' && mapped.trim() ? mapped.trim() : level;
|
|
81
|
+
let fields = {};
|
|
82
|
+
if (level !== null) {
|
|
83
|
+
if (protocol === 'openai-chat') {
|
|
84
|
+
if (wireFormat === 'deepseek') {
|
|
85
|
+
fields =
|
|
86
|
+
level === 'none'
|
|
87
|
+
? { thinking: { type: 'disabled' } }
|
|
88
|
+
: { thinking: { type: 'enabled' }, reasoning_effort: value };
|
|
89
|
+
} else {
|
|
90
|
+
fields = { reasoning_effort: value };
|
|
91
|
+
}
|
|
92
|
+
} else if (protocol === 'openai-responses') {
|
|
93
|
+
fields = { reasoning: { effort: value } };
|
|
94
|
+
} else if (protocol === 'anthropic') {
|
|
95
|
+
fields =
|
|
96
|
+
level === 'none'
|
|
97
|
+
? { thinking: { type: 'disabled' } }
|
|
98
|
+
: { thinking: { type: 'adaptive' }, output_config: { effort: value } };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// An explicit replay flag wins over the catalog's reasoning_content field.
|
|
102
|
+
const replayFlag =
|
|
103
|
+
typeof custom.replay === 'boolean' ? custom.replay : model?.reasoningContentField === 'reasoning_content';
|
|
104
|
+
const replay =
|
|
105
|
+
protocol === 'openai-chat' && replayFlag && thinking !== 'off' && !/reasoner/i.test(String(model?.id || ''));
|
|
106
|
+
return { level, fields, thinking, replay };
|
|
107
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeAnthropicEffortInput } from './anthropic-effort.mjs';
|
|
2
|
+
import { CUSTOM_EFFORT_LEVELS, customEffortLevels, isCustomProviderName } from './custom-reasoning.mjs';
|
|
2
3
|
import { codexModelSupportsEffortUpdates } from './openai-oauth-catalog.mjs';
|
|
3
4
|
|
|
4
5
|
export const EFFORT_CONFIGURATION_BETA = 'mid-conversation-output-config-2026-07-01';
|
|
@@ -42,7 +43,10 @@ export function markEffortConfigurationUnsupported(provider, model) {
|
|
|
42
43
|
|
|
43
44
|
/** True for a provider error that names the mid-conversation effort update. */
|
|
44
45
|
export function isEffortConfigurationRejection(status, text) {
|
|
45
|
-
return
|
|
46
|
+
return (
|
|
47
|
+
status === 400 &&
|
|
48
|
+
/output_config|mid-conversation|configuration_update|reasoning_effort_update/i.test(String(text || ''))
|
|
49
|
+
);
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
@@ -57,7 +61,9 @@ export async function withEffortConfigurationFallback(provider, model, opts, sen
|
|
|
57
61
|
const status = error?.status ?? error?.httpStatus;
|
|
58
62
|
if (opts?._effortConfigurationRetry || !isEffortConfigurationRejection(status, error?.message)) throw error;
|
|
59
63
|
markEffortConfigurationUnsupported(provider, model);
|
|
60
|
-
process.stderr.write(
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`[${provider}] ${model} rejected the mid-conversation effort update; retrying once without it\n`
|
|
66
|
+
);
|
|
61
67
|
return send({ ...opts, effortConfigurationEnabled: false, _effortConfigurationRetry: true });
|
|
62
68
|
}
|
|
63
69
|
}
|
|
@@ -116,6 +122,42 @@ export function effortConfigurationMode(provider, model, opts = {}) {
|
|
|
116
122
|
return null;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// Custom providers on the Chat and Responses wires take a fresh effort with
|
|
126
|
+
// every request, so auto effort sends a turn's or step's effort as that
|
|
127
|
+
// request's own effort: no mid-conversation update, no beta header. A custom
|
|
128
|
+
// Anthropic-wire provider keeps auto effort off.
|
|
129
|
+
const CUSTOM_AUTO_EFFORT_PROTOCOLS = new Set(['openai-chat', 'openai-responses']);
|
|
130
|
+
|
|
131
|
+
/** Auto effort applies to this custom route: a Chat/Responses protocol and a
|
|
132
|
+
* model row declaring a non-empty effort level list. */
|
|
133
|
+
export function customAutoEffortCapable(provider, protocol, model) {
|
|
134
|
+
return (
|
|
135
|
+
isCustomProviderName(provider) &&
|
|
136
|
+
CUSTOM_AUTO_EFFORT_PROTOCOLS.has(protocol) &&
|
|
137
|
+
(customEffortLevels(model)?.length || 0) > 0
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The model auto effort reads its effort options from: a custom provider's
|
|
142
|
+
* effective row (its declared levels), the bare id for every other route. */
|
|
143
|
+
export function autoEffortModel(providerName, providerImpl, model) {
|
|
144
|
+
if (!isCustomProviderName(providerName)) return { id: model };
|
|
145
|
+
return { ...(providerImpl?.getCachedModelInfo?.(model) || {}), id: model };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The effort-change mode of a session's route: 'responses' / 'anthropic'
|
|
149
|
+
* (mid-conversation update), 'custom' (per-request effort), or null. */
|
|
150
|
+
export function turnEffortConfigurationMode(session, provider) {
|
|
151
|
+
const opts = { ...(provider?.config || {}), modelParameters: session.modelParameters || {} };
|
|
152
|
+
const mode = effortConfigurationMode(session.provider, session.model, opts);
|
|
153
|
+
if (mode) return mode;
|
|
154
|
+
const model = autoEffortModel(session.provider, provider, session.model);
|
|
155
|
+
return customAutoEffortCapable(session.provider, provider?.customConfig?.protocol, model) ? 'custom' : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const CUSTOM_EFFORTS = new Set(CUSTOM_EFFORT_LEVELS);
|
|
159
|
+
const effortsFor = (mode) => (mode === 'custom' ? CUSTOM_EFFORTS : EFFORTS);
|
|
160
|
+
|
|
119
161
|
function normalizedEffort(provider, model, effort) {
|
|
120
162
|
if (ANTHROPIC_PROVIDERS.has(provider)) return normalizeAnthropicEffortInput(effort, model) || null;
|
|
121
163
|
const value = String(effort || 'medium')
|
|
@@ -130,19 +172,19 @@ function validSnapshot(value, provider, model, mode) {
|
|
|
130
172
|
value.provider === provider &&
|
|
131
173
|
value.model === modelKey(model) &&
|
|
132
174
|
value.mode === mode &&
|
|
133
|
-
|
|
134
|
-
|
|
175
|
+
effortsFor(mode).has(value.initialEffort) &&
|
|
176
|
+
effortsFor(mode).has(value.effort)
|
|
135
177
|
);
|
|
136
178
|
}
|
|
137
179
|
|
|
138
180
|
// `turnEffort` overrides the session's saved effort for this turn only (auto
|
|
139
|
-
// effort); the saved default itself is never changed here.
|
|
181
|
+
// effort); the saved default itself is never changed here. A custom route's
|
|
182
|
+
// snapshot only records the turn's effort (per-request effort, never projected
|
|
183
|
+
// into the history).
|
|
140
184
|
export function prepareTurnEffortConfiguration(session, provider, turnEffort) {
|
|
141
|
-
const
|
|
142
|
-
const opts = { ...config, modelParameters: session.modelParameters || {} };
|
|
143
|
-
const mode = effortConfigurationMode(session.provider, session.model, opts);
|
|
185
|
+
const mode = turnEffortConfigurationMode(session, provider);
|
|
144
186
|
const effort = normalizedEffort(session.provider, session.model, turnEffort ?? session.effort);
|
|
145
|
-
if (!mode || !
|
|
187
|
+
if (!mode || !effortsFor(mode).has(effort)) return null;
|
|
146
188
|
const first = (session.messages || []).find((message) =>
|
|
147
189
|
validSnapshot(message?.meta?.[META_KEY], session.provider, session.model, mode)
|
|
148
190
|
)?.meta?.[META_KEY];
|
|
@@ -76,7 +76,7 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code }) {
|
|
|
76
76
|
return tokens;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export async function beginOAuthLogin() {
|
|
79
|
+
export async function beginOAuthLogin({ openBrowser = true } = {}) {
|
|
80
80
|
const discovery = await fetchDiscovery();
|
|
81
81
|
const pkce = generatePKCE();
|
|
82
82
|
const state = randomBytes(16).toString('hex');
|
|
@@ -138,6 +138,7 @@ export async function beginOAuthLogin() {
|
|
|
138
138
|
process.stderr.write(
|
|
139
139
|
`\n[grok-oauth] Open this URL to log in (consent shows as "Grok Build"):\n${url.toString()}\n\n`
|
|
140
140
|
);
|
|
141
|
+
if (!openBrowser) return;
|
|
141
142
|
try {
|
|
142
143
|
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
143
144
|
openInBrowser(url.toString());
|
|
@@ -528,16 +528,19 @@ function balanceFromCredits(credits, source) {
|
|
|
528
528
|
if (balance === null) return null;
|
|
529
529
|
return {
|
|
530
530
|
source,
|
|
531
|
-
|
|
532
|
-
|
|
531
|
+
unit: 'credits',
|
|
532
|
+
remainingCredits: round(balance, 4),
|
|
533
|
+
unlimited: credits.unlimited === true,
|
|
534
|
+
hasCredits: credits.has_credits === true,
|
|
533
535
|
};
|
|
534
536
|
}
|
|
535
537
|
|
|
536
538
|
function balanceFromExtraUsage(extra) {
|
|
537
539
|
if (!extra || typeof extra !== 'object') return null;
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
540
|
+
const limitCents = num(extra.monthly_limit, null);
|
|
541
|
+
if (limitCents === null) return null;
|
|
542
|
+
const limit = limitCents / 100;
|
|
543
|
+
const used = num(extra.used_credits, 0) / 100;
|
|
541
544
|
return {
|
|
542
545
|
source: 'anthropic-oauth-extra',
|
|
543
546
|
period: 'monthly',
|
|
@@ -606,7 +609,7 @@ function balanceFromAnthropicSpend(spend) {
|
|
|
606
609
|
return null;
|
|
607
610
|
}
|
|
608
611
|
|
|
609
|
-
function normalizeOpenAIWhamUsage(data) {
|
|
612
|
+
export function normalizeOpenAIWhamUsage(data) {
|
|
610
613
|
const rate = data?.rate_limit && typeof data.rate_limit === 'object' ? data.rate_limit : null;
|
|
611
614
|
if (!rate) return null;
|
|
612
615
|
const windows = [
|
|
@@ -687,13 +690,15 @@ function anthropicLimitWindows(limits, source) {
|
|
|
687
690
|
// The extra-usage (pay-as-you-go) window when it is enabled, else null.
|
|
688
691
|
function anthropicExtraUsageWindow(extraUsage) {
|
|
689
692
|
if (extraUsage?.is_enabled !== true) return null;
|
|
693
|
+
const limitUsd = num(extraUsage.monthly_limit, 0) / 100;
|
|
694
|
+
const usedUsd = num(extraUsage.used_credits, 0) / 100;
|
|
690
695
|
return windowFromPercent(
|
|
691
696
|
'EXTRA',
|
|
692
697
|
{
|
|
693
698
|
utilization: extraUsage.utilization,
|
|
694
|
-
limit_dollars:
|
|
695
|
-
used_dollars:
|
|
696
|
-
remaining_dollars: Math.max(0,
|
|
699
|
+
limit_dollars: limitUsd,
|
|
700
|
+
used_dollars: usedUsd,
|
|
701
|
+
remaining_dollars: Math.max(0, limitUsd - usedUsd),
|
|
697
702
|
},
|
|
698
703
|
'anthropic-oauth-extra'
|
|
699
704
|
);
|
|
@@ -22,6 +22,7 @@ import { applyCompatToolChoice, compatStreamRetryReporter } from './compat-reque
|
|
|
22
22
|
import { ensureChatToolPairs } from './lib/wire-pairing.mjs';
|
|
23
23
|
import { xaiCacheRouting } from './openai-compat-xai.mjs';
|
|
24
24
|
import { preparePdfTextForProvider } from './media-normalization.mjs';
|
|
25
|
+
import { isCustomProviderName, resolveCustomReasoning } from './custom-reasoning.mjs';
|
|
25
26
|
|
|
26
27
|
export async function sendCompatChat(provider, messages, useModel, tools, opts) {
|
|
27
28
|
const signal = opts.signal || null;
|
|
@@ -29,13 +30,31 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
29
30
|
const reason = signal.reason;
|
|
30
31
|
throw reason instanceof Error ? reason : new Error('OpenAI-compat request aborted by session close');
|
|
31
32
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const isCustom = isCustomProviderName(provider.name);
|
|
34
|
+
// Opt-in per-provider wire profile (config.chatProfile): only a wrapper that
|
|
35
|
+
// sets it gets the extra hooks below; every other provider is untouched.
|
|
36
|
+
const profile = provider.config?.chatProfile || null;
|
|
37
|
+
// Custom providers read only their own model rows: a same-id model in the
|
|
38
|
+
// global catalog must not shape a user-defined endpoint's request.
|
|
39
|
+
let modelInfo = null;
|
|
40
|
+
if (isCustom) modelInfo = provider.getCachedModelInfo(useModel) || null;
|
|
41
|
+
else if (provider.name === 'opencode-go')
|
|
42
|
+
modelInfo = provider.getCachedModelInfo(useModel) || getModelMetadataSync(useModel, provider.name);
|
|
43
|
+
else if (profile?.resolveModelInfo) modelInfo = profile.resolveModelInfo(provider, useModel) || null;
|
|
44
|
+
// Resolved before lowering: replay eligibility depends on the snapped level
|
|
45
|
+
// (thinking disabled → nothing to replay).
|
|
46
|
+
const customReasoning = isCustom
|
|
47
|
+
? resolveCustomReasoning({
|
|
48
|
+
protocol: 'openai-chat',
|
|
49
|
+
model: modelInfo,
|
|
50
|
+
effort: opts.effort ?? provider.config?.reasoningEffort,
|
|
51
|
+
})
|
|
52
|
+
: null;
|
|
53
|
+
const replaysReasoningContent = isCustom
|
|
54
|
+
? customReasoning.replay
|
|
55
|
+
: modelInfo?.reasoningContentField === 'reasoning_content' ||
|
|
56
|
+
profile?.replaysReasoningContent === true ||
|
|
57
|
+
(provider.name === 'deepseek' && deepseekReplaysReasoningContent(useModel));
|
|
39
58
|
messages = await preparePdfTextForProvider(messages, provider);
|
|
40
59
|
const params = {
|
|
41
60
|
model: useModel,
|
|
@@ -43,7 +62,11 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
43
62
|
// (cancel/abort) is hard-rejected unpaired, so synthesize the
|
|
44
63
|
// missing tool messages here.
|
|
45
64
|
messages: ensureChatToolPairs(
|
|
46
|
-
toOpenAIMessages(messages, provider.name, {
|
|
65
|
+
toOpenAIMessages(messages, provider.name, {
|
|
66
|
+
replaysReasoningContent,
|
|
67
|
+
...(isCustom ? { customReplay: customReasoning.replay } : {}),
|
|
68
|
+
nativePdf: provider.nativePdf,
|
|
69
|
+
})
|
|
47
70
|
),
|
|
48
71
|
};
|
|
49
72
|
const maxOutputTokens = resolveCompatMaxOutputTokens(opts);
|
|
@@ -51,6 +74,8 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
51
74
|
if (tools?.length) params.tools = toOpenAITools(tools);
|
|
52
75
|
applyCompatToolChoice(params, opts);
|
|
53
76
|
applyCompatProviderChatOptions(params, provider.name, opts, provider.config, modelInfo);
|
|
77
|
+
profile?.applyParams?.(params, { opts, config: provider.config, modelInfo });
|
|
78
|
+
const firstByteTimeoutMs = profile?.firstByteTimeoutMs || PROVIDER_FIRST_BYTE_TIMEOUT_MS;
|
|
54
79
|
const totalSignal = createPassthroughSignal(signal);
|
|
55
80
|
const cacheRouting = provider.name === 'xai' ? xaiCacheRouting(opts, params, tools || [], useModel) : null;
|
|
56
81
|
const cacheRoutingKey = cacheRouting?.key || null;
|
|
@@ -60,7 +85,7 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
60
85
|
try {
|
|
61
86
|
try {
|
|
62
87
|
assembled = await withRetry(
|
|
63
|
-
async ({ signal: attemptSignal }) => {
|
|
88
|
+
refineAttemptErrors(profile, async ({ signal: attemptSignal }) => {
|
|
64
89
|
try {
|
|
65
90
|
opts.onStageChange?.('requesting');
|
|
66
91
|
} catch {
|
|
@@ -79,7 +104,7 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
79
104
|
// owned by the outer withRetry — nesting retry loops
|
|
80
105
|
// here multiplied tail latency (5x5).
|
|
81
106
|
maxAttempts: 1,
|
|
82
|
-
perAttemptTimeoutMs:
|
|
107
|
+
perAttemptTimeoutMs: firstByteTimeoutMs,
|
|
83
108
|
perAttemptLabel: `${provider.name} first byte`,
|
|
84
109
|
}
|
|
85
110
|
);
|
|
@@ -101,6 +126,8 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
101
126
|
// recovered leaked calls only synthesize when they name
|
|
102
127
|
// a tool actually offered to this request.
|
|
103
128
|
knownToolNames: knownToolNamesFromOpenAITools(params.tools),
|
|
129
|
+
...(profile?.firstByteTimeoutMs ? { firstByteTimeoutMs: profile.firstByteTimeoutMs } : {}),
|
|
130
|
+
...(profile?.semanticIdleTimeoutMs ? { semanticIdleTimeoutMs: profile.semanticIdleTimeoutMs } : {}),
|
|
104
131
|
});
|
|
105
132
|
} catch (error) {
|
|
106
133
|
// Grok's reference sampler treats content-idle as a
|
|
@@ -112,7 +139,7 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
112
139
|
}
|
|
113
140
|
throw error;
|
|
114
141
|
}
|
|
115
|
-
},
|
|
142
|
+
}),
|
|
116
143
|
{
|
|
117
144
|
signal: totalSignal.signal,
|
|
118
145
|
onRetry: compatStreamRetryReporter(provider.name, opts),
|
|
@@ -146,6 +173,19 @@ export async function sendCompatChat(provider, messages, useModel, tools, opts)
|
|
|
146
173
|
});
|
|
147
174
|
}
|
|
148
175
|
|
|
176
|
+
// Opt-in: a provider profile may rewrite an attempt's error (gateway detail,
|
|
177
|
+
// permanent-failure stamps) before the retry classifier sees it.
|
|
178
|
+
function refineAttemptErrors(profile, attempt) {
|
|
179
|
+
if (typeof profile?.refineError !== 'function') return attempt;
|
|
180
|
+
return async (ctx) => {
|
|
181
|
+
try {
|
|
182
|
+
return await attempt(ctx);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw profile.refineError(error) || error;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
149
189
|
export async function recoverCompatNonStreaming(provider, { streamErr, params, opts, signal, useModel }) {
|
|
150
190
|
if (!canFallbackNonStreaming(streamErr, { signal })) return null;
|
|
151
191
|
noteErrorUsage(streamErr);
|