mixdog 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/check-voice-runtime-inputs.mjs +25 -0
- package/scripts/gate-local.mjs +12 -3
- package/scripts/latest-guard.mjs +21 -9
- package/scripts/lib/isolated-test-env.mjs +10 -5
- package/scripts/lib/run-node-tests.mjs +37 -37
- package/scripts/lib/scoped-temp.d.mts +16 -0
- package/scripts/lib/scoped-temp.mjs +123 -0
- package/scripts/lib/stale-temp.mjs +44 -0
- package/scripts/post-release-check.mjs +1 -6
- package/scripts/pptx-kit-index.mjs +17 -4
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test-data-dir.mjs +10 -7
- package/src/app.mjs +4 -0
- package/src/defaults/shipped-history.json +5 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -4
- package/src/defaults/skills/computer-use/SKILL.md +4 -2
- package/src/lib/rules-builder.cjs +29 -12
- package/src/output-styles/common.md +2 -2
- package/src/output-styles/simple.md +1 -1
- package/src/rules/agent/AGENT.md +5 -4
- package/src/rules/lead/LEAD.md +10 -5
- package/src/rules/shared/10-tool-workflow.md +3 -1
- package/src/rules/shared/45-destructive.md +0 -2
- package/src/rules/shared/50-execution.md +2 -0
- package/src/rules/shared/70-delivery.md +7 -9
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
- package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
- package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
- package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
- package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
- package/src/runtime/channels/lib/whisper-port.mjs +10 -1
- package/src/runtime/computer-bridge/action-schema.mjs +2 -2
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
- package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
- package/src/runtime/local-provider/server-process.mjs +2 -1
- package/src/runtime/media/adapters/gemini-video.mjs +4 -1
- package/src/runtime/media/adapters/xai-media.mjs +2 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
- package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
- package/src/runtime/office/design/design-art-direction.mjs +29 -27
- package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
- package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
- package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
- package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
- package/src/runtime/office/quality/design-review.mjs +1 -5
- package/src/runtime/office/quality/document-brief.mjs +1 -5
- package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
- package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
- package/src/runtime/shared/bot-owned-work.mjs +56 -0
- package/src/runtime/shared/defaults-separation.mjs +1 -4
- package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
- package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
- package/src/runtime/shared/handoff-artifacts.mjs +18 -8
- package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
- package/src/runtime/shared/llm/cost.mjs +5 -1
- package/src/runtime/shared/loopback-listener.mjs +25 -2
- package/src/runtime/shared/model-route-chain.d.mts +7 -2
- package/src/runtime/shared/model-route-chain.mjs +22 -4
- package/src/runtime/shared/open-url.mjs +10 -0
- package/src/runtime/shared/session-activity-signal.mjs +14 -0
- package/src/runtime/shared/session-scratch.mjs +37 -0
- package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
- package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +3 -0
- package/src/runtime/shared/tool-work-units.mjs +2 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
- package/src/runtime/shared/worktrees/contract.mjs +4 -1
- package/src/runtime/shared/worktrees/git.mjs +15 -0
- package/src/runtime/shared/worktrees/manager.mjs +87 -38
- package/src/runtime/shared/worktrees/test-support.mjs +6 -10
- package/src/runtime/visualize-bridge/client.mjs +2 -0
- package/src/session-runtime/agent-home-delivery.mjs +41 -0
- package/src/session-runtime/agent-task-sessions.mjs +152 -63
- package/src/session-runtime/agent-tool-routing.mjs +8 -41
- package/src/session-runtime/boot/tools.mjs +5 -1
- package/src/session-runtime/bot-status.mjs +12 -6
- package/src/session-runtime/bot-target.mjs +38 -0
- package/src/session-runtime/channel-config-api.mjs +6 -1
- package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
- package/src/session-runtime/conversation-import/discovery.mjs +271 -25
- package/src/session-runtime/conversation-import/service.mjs +26 -5
- package/src/session-runtime/handoff-attachments.mjs +82 -31
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
- package/src/session-runtime/model-route/set-route.mjs +11 -2
- package/src/session-runtime/provider-catalog-cache.mjs +82 -36
- package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
- package/src/session-runtime/provider-models.mjs +4 -2
- package/src/session-runtime/resource-recall-api.mjs +6 -1
- package/src/session-runtime/runtime-feature-gates.mjs +5 -1
- package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
- package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
- package/src/session-runtime/services/agent-tool/render.mjs +71 -17
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
- package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
- package/src/session-runtime/services/provider-admin.mjs +2 -1
- package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
- package/src/session-runtime/workspace/transitions.mjs +25 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
- package/src/standalone/daemon-session-control.mjs +18 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
- package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
- package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
- package/src/tui/app/app-view/floating-panel.jsx +7 -4
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
- package/src/tui/app/model-picker/route-selection.mjs +5 -6
- package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
- package/src/tui/app/provider-setup/provider-items.mjs +8 -2
- package/src/tui/app/provider-setup-picker.mjs +9 -5
- package/src/tui/components/QuestionPanel.jsx +13 -4
- package/src/tui/dist/index.mjs +75 -28
- package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
- package/src/ui/statusline-agents.mjs +1 -3
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// A pasted key or pasted callback JSON completes the same login by hand. The
|
|
4
4
|
// first sign-in on a machine adopts an existing Command Code CLI login instead.
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
|
+
import { listenOn } from './lib/listen-on.mjs';
|
|
6
7
|
import { randomBytes } from 'node:crypto';
|
|
7
8
|
import { readFileSync } from 'node:fs';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
@@ -95,16 +96,6 @@ function readLocalAuth(path) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
function listenOn(server, port, host) {
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
server.once('error', reject);
|
|
101
|
-
server.listen(port, host, () => {
|
|
102
|
-
server.off('error', reject);
|
|
103
|
-
resolve();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
99
|
function readBody(req) {
|
|
109
100
|
return new Promise((resolve, reject) => {
|
|
110
101
|
const chunks = [];
|
|
@@ -677,6 +677,9 @@ class CursorProviderBase {
|
|
|
677
677
|
return await consumeCompatChatCompletionStream(responseSseEvents(response, signal), {
|
|
678
678
|
signal,
|
|
679
679
|
label: this.name,
|
|
680
|
+
// The wrapper below always exists; the stream joins the session's
|
|
681
|
+
// logical request only when the caller reports progress into it.
|
|
682
|
+
sessionId: typeof sendOpts.onStreamDelta === 'function' ? sendOpts.sessionId : null,
|
|
680
683
|
onStreamDelta: (delta) => {
|
|
681
684
|
sendOpts.onStreamDelta?.(delta);
|
|
682
685
|
},
|
|
@@ -193,7 +193,7 @@ function connectError(
|
|
|
193
193
|
const error = new DevinWireError(text, {
|
|
194
194
|
code: finalCode,
|
|
195
195
|
// A reworded throttle reports its throttle status, not the carrier's.
|
|
196
|
-
status: tooLarge ? 413 :
|
|
196
|
+
status: tooLarge ? 413 : reworded ? mappedStatus : status || mappedStatus,
|
|
197
197
|
retryable: tooLarge ? false : retryable,
|
|
198
198
|
connectCode: code,
|
|
199
199
|
detail: [detail, detailsSummary].filter(Boolean).join(' '),
|
|
@@ -405,6 +405,9 @@ export class DevinOAuthProvider {
|
|
|
405
405
|
signal,
|
|
406
406
|
label: this.name,
|
|
407
407
|
providerName: this.name,
|
|
408
|
+
// The wrapper below always exists; the stream joins the session's
|
|
409
|
+
// logical request only when the caller reports progress into it.
|
|
410
|
+
sessionId: typeof sendOpts.onStreamDelta === 'function' ? sendOpts.sessionId : null,
|
|
408
411
|
onStreamDelta: (delta) => {
|
|
409
412
|
if (!streaming) {
|
|
410
413
|
streaming = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Grok OAuth browser login + PKCE exchange against xAI's shared OAuth client
|
|
2
|
-
// (the consent screen renders it as "Grok Build").
|
|
2
|
+
// (the consent screen renders it as "Grok Build"). A machine with no browser
|
|
3
|
+
// (`openBrowser: false`) signs in by RFC 8628 device code instead.
|
|
3
4
|
import { createServer } from 'node:http';
|
|
4
5
|
import { randomBytes } from 'node:crypto';
|
|
5
6
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
@@ -22,10 +23,44 @@ import {
|
|
|
22
23
|
_scrubTokens,
|
|
23
24
|
} from './grok-oauth-tokens.mjs';
|
|
24
25
|
|
|
26
|
+
// The device flow is xAI's own headless path (`grok login --device-auth`): the
|
|
27
|
+
// user opens the URL on any device and confirms the code while this host polls,
|
|
28
|
+
// so no loopback listener, port forward or pasted redirect is involved.
|
|
29
|
+
const DEVICE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
30
|
+
// The path the issuer's discovery document advertises as its
|
|
31
|
+
// device_authorization_endpoint; the token endpoint is the discovered one.
|
|
32
|
+
const DEVICE_CODE_PATH = '/oauth2/device/code';
|
|
33
|
+
const DEFAULT_DEVICE_INTERVAL_S = 5;
|
|
34
|
+
const SLOW_DOWN_STEP_S = 5;
|
|
35
|
+
// The Grok CLI keeps polling for at least ten minutes whatever expires_in says.
|
|
36
|
+
const MIN_DEVICE_EXPIRY_S = 600;
|
|
37
|
+
const SAFE_USER_CODE = /^[A-Za-z0-9-]+$/;
|
|
38
|
+
|
|
25
39
|
export function generatePKCE() {
|
|
26
40
|
return createOAuthPkce();
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
// The tokens a successful grant (authorization code or device code) yields.
|
|
44
|
+
function tokensFromGrant(json, discovery) {
|
|
45
|
+
if (!json.access_token || !json.refresh_token) {
|
|
46
|
+
throw new Error('[grok-oauth] token exchange response missing access_token or refresh_token');
|
|
47
|
+
}
|
|
48
|
+
const identity = _identityFromAccessToken(json.access_token);
|
|
49
|
+
return {
|
|
50
|
+
access_token: json.access_token,
|
|
51
|
+
refresh_token: json.refresh_token,
|
|
52
|
+
expires_at:
|
|
53
|
+
typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : _normalizeExpiresAt(json.expires_at),
|
|
54
|
+
token_endpoint: discovery.token_endpoint,
|
|
55
|
+
user_id: identity.user_id || '',
|
|
56
|
+
principal_type: json.principal_type || identity.principal_type || '',
|
|
57
|
+
principal_id: json.principal_id || identity.principal_id || '',
|
|
58
|
+
...(emailFromJwts(json.id_token, json.access_token)
|
|
59
|
+
? { email: emailFromJwts(json.id_token, json.access_token) }
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
29
64
|
export async function exchangeAuthorizationCode({ discovery, pkce, code, signal = null }) {
|
|
30
65
|
signal?.throwIfAborted();
|
|
31
66
|
const cleanCode = String(code || '').trim();
|
|
@@ -57,32 +92,187 @@ export async function exchangeAuthorizationCode({ discovery, pkce, code, signal
|
|
|
57
92
|
const text = await tokenRes.text().catch(() => '');
|
|
58
93
|
throw new Error(`[grok-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`);
|
|
59
94
|
}
|
|
60
|
-
const
|
|
61
|
-
if (!json.access_token || !json.refresh_token) {
|
|
62
|
-
throw new Error('[grok-oauth] token exchange response missing access_token or refresh_token');
|
|
63
|
-
}
|
|
64
|
-
const identity = _identityFromAccessToken(json.access_token);
|
|
65
|
-
const tokens = {
|
|
66
|
-
access_token: json.access_token,
|
|
67
|
-
refresh_token: json.refresh_token,
|
|
68
|
-
expires_at:
|
|
69
|
-
typeof json.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : _normalizeExpiresAt(json.expires_at),
|
|
70
|
-
token_endpoint: discovery.token_endpoint,
|
|
71
|
-
user_id: identity.user_id || '',
|
|
72
|
-
principal_type: json.principal_type || identity.principal_type || '',
|
|
73
|
-
principal_id: json.principal_id || identity.principal_id || '',
|
|
74
|
-
...(emailFromJwts(json.id_token, json.access_token)
|
|
75
|
-
? { email: emailFromJwts(json.id_token, json.access_token) }
|
|
76
|
-
: {}),
|
|
77
|
-
};
|
|
95
|
+
const tokens = tokensFromGrant(await tokenRes.json(), discovery);
|
|
78
96
|
// A cancelled login never stores the account it was exchanging.
|
|
79
97
|
signal?.throwIfAborted();
|
|
80
98
|
saveTokens(tokens);
|
|
81
99
|
return tokens;
|
|
82
100
|
}
|
|
83
101
|
|
|
84
|
-
|
|
102
|
+
// Not sleepWithAbort: grok-oauth-isolated.test.mjs loads this file in a VM
|
|
103
|
+
// sandbox that admits no dependency beyond its reviewed list.
|
|
104
|
+
const defaultSleep = (ms, signal) =>
|
|
105
|
+
new Promise((resolve, reject) => {
|
|
106
|
+
if (signal.aborted) return reject(signal.reason);
|
|
107
|
+
const onAbort = () => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
reject(signal.reason);
|
|
110
|
+
};
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
signal.removeEventListener('abort', onAbort);
|
|
113
|
+
resolve();
|
|
114
|
+
}, ms);
|
|
115
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const hasAsciiControl = (text) => [...text].some((char) => char.charCodeAt(0) < 0x20 || char.charCodeAt(0) === 0x7f);
|
|
119
|
+
|
|
120
|
+
// The Grok CLI's verification-URI rules (validate_verification_uri): a string
|
|
121
|
+
// without ASCII control characters (they could reach the terminal) that parses
|
|
122
|
+
// as a URL and is https, or http only for localhost / 127.0.0.1.
|
|
123
|
+
function verificationUri(value, field) {
|
|
124
|
+
if (typeof value !== 'string') throw new Error(`[grok-oauth] device authorization ${field} must be a string`);
|
|
125
|
+
if (hasAsciiControl(value) || !URL.canParse(value)) {
|
|
126
|
+
throw new Error('[grok-oauth] device authorization returned an invalid verification URI');
|
|
127
|
+
}
|
|
128
|
+
const { protocol, hostname } = new URL(value);
|
|
129
|
+
const loopback = protocol === 'http:' && (hostname === 'localhost' || hostname === '127.0.0.1');
|
|
130
|
+
if (protocol !== 'https:' && !loopback) {
|
|
131
|
+
throw new Error('[grok-oauth] device authorization returned an unsupported verification URI scheme');
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The vendor reads these as typed integers: a string or a fraction is a
|
|
137
|
+
// malformed response.
|
|
138
|
+
function integerField(value, field) {
|
|
139
|
+
if (!Number.isInteger(value)) {
|
|
140
|
+
throw new Error(`[grok-oauth] device authorization ${field} must be an integer`);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const requestSignal = (signal) => AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)]);
|
|
146
|
+
|
|
147
|
+
function postDeviceForm(url, params, signal) {
|
|
148
|
+
return fetch(url, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
151
|
+
body: new URLSearchParams(params),
|
|
152
|
+
// The device code is secret-bearing: refuse redirects.
|
|
153
|
+
redirect: 'error',
|
|
154
|
+
signal: requestSignal(signal),
|
|
155
|
+
dispatcher: getLlmDispatcher(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// null when the issuer has no device flow (404), as the Grok CLI reads it:
|
|
160
|
+
// the caller then falls back to the loopback login.
|
|
161
|
+
async function requestDeviceCode(discovery, signal) {
|
|
162
|
+
const res = await postDeviceForm(
|
|
163
|
+
new URL(DEVICE_CODE_PATH, discovery.token_endpoint).href,
|
|
164
|
+
{ client_id: CLIENT_ID, scope: SCOPE },
|
|
165
|
+
signal
|
|
166
|
+
);
|
|
167
|
+
if (res.status === 404) return null;
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const text = await res.text().catch(() => '');
|
|
170
|
+
throw new Error(`[grok-oauth] device authorization ${res.status}: ${_scrubTokens(text).slice(0, 300)}`);
|
|
171
|
+
}
|
|
172
|
+
const json = await res.json().catch(() => null);
|
|
173
|
+
const deviceCode = typeof json?.device_code === 'string' ? json.device_code : '';
|
|
174
|
+
const userCode = typeof json?.user_code === 'string' ? json.user_code : '';
|
|
175
|
+
if (!deviceCode || !SAFE_USER_CODE.test(userCode)) {
|
|
176
|
+
throw new Error('[grok-oauth] device authorization response is missing or has invalid required fields');
|
|
177
|
+
}
|
|
178
|
+
// Both addresses are checked on their own: an invalid one fails the login
|
|
179
|
+
// rather than yielding to the other. The complete one (code pre-filled) wins.
|
|
180
|
+
const plainUrl = verificationUri(json.verification_uri, 'verification_uri');
|
|
181
|
+
const completeUrl = json.verification_uri_complete ?? null;
|
|
182
|
+
const verificationUrl = completeUrl === null ? plainUrl : verificationUri(completeUrl, 'verification_uri_complete');
|
|
183
|
+
const interval = json.interval ?? null;
|
|
184
|
+
return {
|
|
185
|
+
deviceCode,
|
|
186
|
+
userCode,
|
|
187
|
+
verificationUrl,
|
|
188
|
+
// 5 s only when the interval is omitted; zero or a negative value clamps to one second.
|
|
189
|
+
intervalS: Math.max(interval === null ? DEFAULT_DEVICE_INTERVAL_S : integerField(interval, 'interval'), 1),
|
|
190
|
+
expiresInS: Math.max(integerField(json.expires_in, 'expires_in'), MIN_DEVICE_EXPIRY_S),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const deviceExpired = () => new Error('[grok-oauth] device code expired — start the login again');
|
|
195
|
+
|
|
196
|
+
/** Poll until approval, denial or expiry; resolves the token response of the grant. */
|
|
197
|
+
async function pollDeviceGrant(device, discovery, { signal, sleep, now }) {
|
|
198
|
+
const deadline = now() + device.expiresInS * 1000;
|
|
199
|
+
let intervalMs = device.intervalS * 1000;
|
|
200
|
+
for (;;) {
|
|
201
|
+
// A poll straight after the code was issued only answers authorization_pending.
|
|
202
|
+
await sleep(intervalMs, signal);
|
|
203
|
+
if (now() > deadline) throw deviceExpired();
|
|
204
|
+
let res;
|
|
205
|
+
let text;
|
|
206
|
+
try {
|
|
207
|
+
res = await postDeviceForm(
|
|
208
|
+
discovery.token_endpoint,
|
|
209
|
+
{ grant_type: DEVICE_GRANT_TYPE, device_code: device.deviceCode, client_id: CLIENT_ID },
|
|
210
|
+
signal
|
|
211
|
+
);
|
|
212
|
+
text = await res.text();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// As in the Grok CLI, a transport failure ends the login at once.
|
|
215
|
+
throw new Error(`[grok-oauth] device token request failed: ${_scrubTokens(String(err?.message || err))}`);
|
|
216
|
+
}
|
|
217
|
+
let json = null;
|
|
218
|
+
try {
|
|
219
|
+
json = text ? JSON.parse(text) : null;
|
|
220
|
+
} catch {
|
|
221
|
+
/* a non-JSON body is reported below */
|
|
222
|
+
}
|
|
223
|
+
if (res.ok && json?.access_token) return json;
|
|
224
|
+
const error = typeof json?.error === 'string' ? json.error : '';
|
|
225
|
+
// Only these two answers keep the poll going; anything else ends the login.
|
|
226
|
+
if (error === 'slow_down') {
|
|
227
|
+
intervalMs += SLOW_DOWN_STEP_S * 1000;
|
|
228
|
+
} else if (error === 'access_denied') {
|
|
229
|
+
throw new Error('[grok-oauth] sign-in was denied');
|
|
230
|
+
} else if (error === 'expired_token') {
|
|
231
|
+
throw deviceExpired();
|
|
232
|
+
} else if (error !== 'authorization_pending') {
|
|
233
|
+
throw new Error(`[grok-oauth] device token ${res.status}: ${error || _scrubTokens(text).slice(0, 300)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function beginDeviceLogin(discovery, { sleep, now }) {
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
const device = await requestDeviceCode(discovery, controller.signal);
|
|
241
|
+
if (!device) return null;
|
|
242
|
+
process.stderr.write(
|
|
243
|
+
`\n[grok-oauth] Enter code: ${device.userCode}\n[grok-oauth] Open: ${device.verificationUrl}\n\n`
|
|
244
|
+
);
|
|
245
|
+
const waitForCallback = pollDeviceGrant(device, discovery, { signal: controller.signal, sleep, now })
|
|
246
|
+
.then((json) => {
|
|
247
|
+
const tokens = tokensFromGrant(json, discovery);
|
|
248
|
+
// A cancelled login never stores the account it was granted.
|
|
249
|
+
controller.signal.throwIfAborted();
|
|
250
|
+
saveTokens(tokens);
|
|
251
|
+
return tokens;
|
|
252
|
+
})
|
|
253
|
+
.catch((error) => {
|
|
254
|
+
// Cancelling settles quietly, as it does for the loopback login.
|
|
255
|
+
if (controller.signal.aborted) return null;
|
|
256
|
+
throw error;
|
|
257
|
+
});
|
|
258
|
+
// A caller may only ever cancel; a later failure must not be unhandled.
|
|
259
|
+
waitForCallback.catch(() => {});
|
|
260
|
+
return {
|
|
261
|
+
provider: 'grok-oauth',
|
|
262
|
+
url: device.verificationUrl,
|
|
263
|
+
manualUrl: null,
|
|
264
|
+
userCode: device.userCode,
|
|
265
|
+
waitForCallback,
|
|
266
|
+
cancel: () => controller.abort(new Error('Grok login cancelled')),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function beginOAuthLogin({ openBrowser = true, sleep = defaultSleep, now = Date.now } = {}) {
|
|
85
271
|
const discovery = await fetchDiscovery();
|
|
272
|
+
if (!openBrowser) {
|
|
273
|
+
const device = await beginDeviceLogin(discovery, { sleep, now });
|
|
274
|
+
if (device) return device;
|
|
275
|
+
}
|
|
86
276
|
const pkce = generatePKCE();
|
|
87
277
|
const state = randomBytes(16).toString('hex');
|
|
88
278
|
const nonce = randomBytes(16).toString('hex');
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Diagnostic per-block prefix fingerprint of an Anthropic request: the tools,
|
|
2
|
-
// each system block (BP1/BP2/BP3/env, the routing prefix included)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// each system block (BP1/BP2/BP3/env, the routing prefix included), the
|
|
3
|
+
// thinking parameters (they key the message cache), the cache_control marker
|
|
4
|
+
// positions and, at each marked message, a chained hash of the messages up to
|
|
5
|
+
// it — as short hashes only, never any text, so it is safe in shipped builds.
|
|
6
|
+
// The last two requests per session are kept in memory so a usage-detected
|
|
7
|
+
// cache miss (session/loop/send-phase.mjs) can say which block, marker or
|
|
8
|
+
// message breakpoint moved. The latest one also rides on the session record
|
|
9
|
+
// (persistableAnthropicPrefix / seedAnthropicPrefix), so the first request
|
|
10
|
+
// after a restart is compared with the last one before it.
|
|
7
11
|
import { createHash } from 'node:crypto';
|
|
8
12
|
|
|
9
13
|
const HASH_CHARS = 12;
|
|
@@ -22,9 +26,26 @@ const withoutMarker = (block) => {
|
|
|
22
26
|
return rest;
|
|
23
27
|
};
|
|
24
28
|
|
|
29
|
+
// A message as the cache reads it: markers stripped, a string content as the
|
|
30
|
+
// one text block a marker would turn it into.
|
|
31
|
+
const messageWithoutMarkers = (message) => {
|
|
32
|
+
const content =
|
|
33
|
+
typeof message?.content === 'string' ? [{ type: 'text', text: message.content }] : message?.content || [];
|
|
34
|
+
return { ...message, content: Array.isArray(content) ? content.map(withoutMarker) : content };
|
|
35
|
+
};
|
|
36
|
+
|
|
25
37
|
const markerTtl = (marker) => (marker?.ttl ? String(marker.ttl) : '5m');
|
|
26
38
|
|
|
27
|
-
/**
|
|
39
|
+
/** The session id a request's prefix is recorded under: the visible Mixdog
|
|
40
|
+
* session, not the account-scoped id an account roster sends under. */
|
|
41
|
+
export function anthropicPrefixSessionId(opts) {
|
|
42
|
+
return opts?.usageSessionId || opts?.sessionId || opts?.session?.id || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `{ tools, system: [hash…], params, markers: ['s1:1h', 'm12:5m', …],
|
|
46
|
+
* messageMarks: [{ i, h }] }` of a request body; the full message hash chain
|
|
47
|
+
* rides along non-enumerable (memory only) for the next request's
|
|
48
|
+
* comparison. */
|
|
28
49
|
export function anthropicPrefixFingerprint(body) {
|
|
29
50
|
const system = Array.isArray(body?.system) ? body.system : [];
|
|
30
51
|
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
|
@@ -32,12 +53,47 @@ export function anthropicPrefixFingerprint(body) {
|
|
|
32
53
|
system.forEach((block, index) => {
|
|
33
54
|
if (block?.cache_control) markers.push(`s${index}:${markerTtl(block.cache_control)}`);
|
|
34
55
|
});
|
|
56
|
+
const params = hash({ thinking: body?.thinking ?? null, output_config: body?.output_config ?? null });
|
|
57
|
+
const chain = [];
|
|
58
|
+
const messageMarks = [];
|
|
59
|
+
let link = params;
|
|
35
60
|
messages.forEach((message, index) => {
|
|
61
|
+
link = hash([link, messageWithoutMarkers(message)]);
|
|
62
|
+
chain.push(link);
|
|
36
63
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
37
64
|
const marked = content.find((block) => block?.cache_control);
|
|
38
|
-
if (marked)
|
|
65
|
+
if (marked) {
|
|
66
|
+
markers.push(`m${index}:${markerTtl(marked.cache_control)}`);
|
|
67
|
+
messageMarks.push({ i: index, h: link });
|
|
68
|
+
}
|
|
39
69
|
});
|
|
40
|
-
|
|
70
|
+
const fingerprint = {
|
|
71
|
+
tools: hash(body?.tools || []),
|
|
72
|
+
system: system.map((block) => hash(withoutMarker(block))),
|
|
73
|
+
params,
|
|
74
|
+
markers,
|
|
75
|
+
messageMarks,
|
|
76
|
+
};
|
|
77
|
+
Object.defineProperty(fingerprint, 'chain', { value: chain, enumerable: false });
|
|
78
|
+
return fingerprint;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `fingerprint` as the session record keeps it (hashes only, no chain). */
|
|
82
|
+
export function persistableAnthropicPrefix(fingerprint) {
|
|
83
|
+
if (!fingerprint) return null;
|
|
84
|
+
const { tools, system, params, markers, messageMarks } = fingerprint;
|
|
85
|
+
return { tools, system, params, markers, messageMarks };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A restarted process has no previous request for the session: take the
|
|
89
|
+
* one its record kept (persistableAnthropicPrefix) as the latest, so the
|
|
90
|
+
* next request is compared with it. A no-op once the session has one. */
|
|
91
|
+
export function seedAnthropicPrefix(sessionId, fingerprint) {
|
|
92
|
+
if (!sessionId || !fingerprint || typeof fingerprint !== 'object') return;
|
|
93
|
+
const key = String(sessionId);
|
|
94
|
+
if (bySession.has(key)) return;
|
|
95
|
+
bySession.set(key, { previous: null, current: persistableAnthropicPrefix(fingerprint) });
|
|
96
|
+
if (bySession.size > MAX_SESSIONS) bySession.delete(bySession.keys().next().value);
|
|
41
97
|
}
|
|
42
98
|
|
|
43
99
|
/** Keep `fingerprint` as the session's latest request (the one before it stays as previous). */
|
|
@@ -55,8 +111,11 @@ export function latestAnthropicPrefix(sessionId) {
|
|
|
55
111
|
return bySession.get(String(sessionId || ''))?.current || null;
|
|
56
112
|
}
|
|
57
113
|
|
|
58
|
-
/** `{ changedBlocks: 'tools,s3' | 'none', markersChanged
|
|
59
|
-
* last two requests, or
|
|
114
|
+
/** `{ changedBlocks: 'tools,s3,params' | 'none', markersChanged,
|
|
115
|
+
* lostMessageBreakpoints? }` between the session's last two requests, or
|
|
116
|
+
* null before it has two. `lostMessageBreakpoints` ('m12,m40' | 'none')
|
|
117
|
+
* names the previous request's message breakpoints whose prefix the latest
|
|
118
|
+
* request does not repeat (absent when either side predates the chain). */
|
|
60
119
|
export function anthropicPrefixChanges(sessionId) {
|
|
61
120
|
const entry = bySession.get(String(sessionId || ''));
|
|
62
121
|
if (!entry?.previous) return null;
|
|
@@ -67,10 +126,16 @@ export function anthropicPrefixChanges(sessionId) {
|
|
|
67
126
|
for (let index = 0; index < blocks; index += 1) {
|
|
68
127
|
if (previous.system[index] !== current.system[index]) changed.push(`s${index}`);
|
|
69
128
|
}
|
|
129
|
+
if (previous.params && current.params && previous.params !== current.params) changed.push('params');
|
|
70
130
|
// Message markers move every turn; only the system markers are a layout change.
|
|
71
131
|
const systemMarkers = (print) => print.markers.filter((marker) => marker.startsWith('s')).join(',');
|
|
72
|
-
|
|
132
|
+
const changes = {
|
|
73
133
|
changedBlocks: changed.length ? changed.join(',') : 'none',
|
|
74
134
|
markersChanged: systemMarkers(previous) !== systemMarkers(current),
|
|
75
135
|
};
|
|
136
|
+
if (Array.isArray(previous.messageMarks) && Array.isArray(current.chain)) {
|
|
137
|
+
const lost = previous.messageMarks.filter(({ i, h }) => current.chain[i] !== h).map(({ i }) => `m${i}`);
|
|
138
|
+
changes.lostMessageBreakpoints = lost.length ? lost.join(',') : 'none';
|
|
139
|
+
}
|
|
140
|
+
return changes;
|
|
76
141
|
}
|
|
@@ -108,11 +108,16 @@ export function appendAnthropicTailReminder(anthropicMessages, reminder) {
|
|
|
108
108
|
// 5.5, Sonnet 5.5) and with explicit lists naming Sonnet 5 or Opus 4.8, and
|
|
109
109
|
// fallback runs only on a safety-classifier refusal.
|
|
110
110
|
export function anthropicTurnScopedReminders(provider, model, config = {}) {
|
|
111
|
+
return anthropicFirstPartyRoute(provider, config) && supportsAnthropicTurnScopedSystemMessages(model);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The first-party Messages routes: native provider, beta headers allowed, no
|
|
115
|
+
// gateway base URL. Request features a proxy answers with a 400 are gated here.
|
|
116
|
+
export function anthropicFirstPartyRoute(provider, config = {}) {
|
|
111
117
|
return (
|
|
112
118
|
ANTHROPIC_NATIVE_PROVIDERS.has(provider) &&
|
|
113
119
|
config?.disableBetaHeaders !== true &&
|
|
114
|
-
(!config?.baseURL || /^https:\/\/api\.anthropic\.com(?:\/|$)/.test(config.baseURL))
|
|
115
|
-
supportsAnthropicTurnScopedSystemMessages(model)
|
|
120
|
+
(!config?.baseURL || /^https:\/\/api\.anthropic\.com(?:\/|$)/.test(config.baseURL))
|
|
116
121
|
);
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -333,6 +338,9 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
333
338
|
const ownProvider = String(provider || '').toLowerCase();
|
|
334
339
|
const availableNames = anthropicToolNameSet(availableTools);
|
|
335
340
|
const result = [];
|
|
341
|
+
// Tool-result user messages a provider request ended with (see the
|
|
342
|
+
// `sentRequestTail` meta finalizeInterruptedTurn stamps).
|
|
343
|
+
const sent = new WeakSet();
|
|
336
344
|
for (let idx = 0; idx < messages.length; idx++) {
|
|
337
345
|
const m = messages[idx];
|
|
338
346
|
if (m.role === 'system') continue;
|
|
@@ -407,6 +415,17 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
407
415
|
} else {
|
|
408
416
|
result.push({ role: 'user', content: [block, ...hoisted] });
|
|
409
417
|
}
|
|
418
|
+
if (m.meta?.sentRequestTail === true) sent.add(result[result.length - 1]);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
// Text after a tool_result message a provider request already carried
|
|
422
|
+
// (an interrupted request's tail: the restart or cancel marker, the next
|
|
423
|
+
// prompt and anything riding with it) goes in as sibling blocks of that
|
|
424
|
+
// user message. Folding it into the tool_result would rewrite a block the
|
|
425
|
+
// provider cached, so the whole message prefix would be written again.
|
|
426
|
+
const tail = result[result.length - 1];
|
|
427
|
+
if (m.role === 'user' && m.meta?.source !== 'steering' && sent.has(tail)) {
|
|
428
|
+
tail.content.push(...siblingBlocks(normalizeContentForAnthropic(m.content)));
|
|
410
429
|
continue;
|
|
411
430
|
}
|
|
412
431
|
// First-party client parity: fold a user text turn that directly follows a
|
|
@@ -424,6 +443,8 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
424
443
|
!isSteering &&
|
|
425
444
|
foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))
|
|
426
445
|
) {
|
|
446
|
+
// A reminder folded before the request was sent ends that request.
|
|
447
|
+
if (m.meta?.sentRequestTail === true) sent.add(tail);
|
|
427
448
|
continue;
|
|
428
449
|
}
|
|
429
450
|
// An assistant turn with no text and no blocks (a refused or empty final
|
|
@@ -436,6 +457,15 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
436
457
|
return sanitizeAnthropicContentPairs(result);
|
|
437
458
|
}
|
|
438
459
|
|
|
460
|
+
// A user message's content as blocks of the message it joins; blank text is
|
|
461
|
+
// dropped (a blank text block is a hard 400).
|
|
462
|
+
function siblingBlocks(content) {
|
|
463
|
+
let blocks = [];
|
|
464
|
+
if (typeof content === 'string') blocks = [{ type: 'text', text: content }];
|
|
465
|
+
else if (Array.isArray(content)) blocks = content;
|
|
466
|
+
return blocks.filter((block) => !(block?.type === 'text' && isBlankText(block.text)));
|
|
467
|
+
}
|
|
468
|
+
|
|
439
469
|
function isBlankText(content) {
|
|
440
470
|
return content == null || (typeof content === 'string' && !content.trim());
|
|
441
471
|
}
|
|
@@ -619,7 +649,10 @@ export function anthropicToolExclusion(tool, logTag = 'anthropic') {
|
|
|
619
649
|
|
|
620
650
|
// A Messages endpoint rejects the whole request for one invalid input_schema,
|
|
621
651
|
// so a tool broken even after lowering is reported and left out.
|
|
622
|
-
|
|
652
|
+
// `eager`: fine-grained tool streaming — the API streams each tool parameter as
|
|
653
|
+
// it is generated instead of buffering it until complete, so a large tool input
|
|
654
|
+
// does not leave the stream silent. First-party routes only.
|
|
655
|
+
function toAnthropicTools(tools, logTag, onToolExcluded, eager = false) {
|
|
623
656
|
const out = [];
|
|
624
657
|
tools.forEach((tool, index) => {
|
|
625
658
|
const result = anthropicInputSchema(tool, logTag);
|
|
@@ -629,6 +662,7 @@ function toAnthropicTools(tools, logTag, onToolExcluded) {
|
|
|
629
662
|
}
|
|
630
663
|
const entry = { name: tool.name, description: tool.description, input_schema: result.schema };
|
|
631
664
|
if (tool.deferLoading === true || tool.defer_loading === true) entry.defer_loading = true;
|
|
665
|
+
if (eager) entry.eager_input_streaming = true;
|
|
632
666
|
out.push(entry);
|
|
633
667
|
});
|
|
634
668
|
return out;
|
|
@@ -727,20 +761,25 @@ export function deferredAnthropicTools(activeTools, messages, opts, provider) {
|
|
|
727
761
|
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
728
762
|
}
|
|
729
763
|
|
|
730
|
-
|
|
764
|
+
// `config`: the provider config (baseURL, disableBetaHeaders) deciding the route.
|
|
765
|
+
export function requestAnthropicTools(tools, messages, opts, provider, config = {}) {
|
|
731
766
|
const activeTools = Array.isArray(tools) ? tools : [];
|
|
767
|
+
const eager = anthropicFirstPartyRoute(provider, config);
|
|
732
768
|
if (opts?.providerToolSnapshotAuthoritative === true) {
|
|
733
769
|
const nativePrefixCount = providerNativeToolPrefixCount(activeTools, opts.providerNativeToolPrefixCount);
|
|
734
770
|
return [
|
|
735
771
|
...activeTools.slice(0, nativePrefixCount),
|
|
736
|
-
...toAnthropicTools(activeTools.slice(nativePrefixCount), provider, opts.onToolExcluded),
|
|
772
|
+
...toAnthropicTools(activeTools.slice(nativePrefixCount), provider, opts.onToolExcluded, eager),
|
|
737
773
|
];
|
|
738
774
|
}
|
|
739
775
|
const deferredTools = deferredAnthropicTools(activeTools, messages, opts, provider);
|
|
740
776
|
const nativeTools = Array.isArray(opts?.nativeTools)
|
|
741
777
|
? opts.nativeTools.filter((tool) => tool && typeof tool === 'object')
|
|
742
778
|
: [];
|
|
743
|
-
return [
|
|
779
|
+
return [
|
|
780
|
+
...nativeTools,
|
|
781
|
+
...toAnthropicTools([...activeTools, ...deferredTools], provider, opts?.onToolExcluded, eager),
|
|
782
|
+
];
|
|
744
783
|
}
|
|
745
784
|
|
|
746
785
|
// Applies cache_control markers to the FINAL, already-sanitized Anthropic
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The first-output window one provider stream runs under. A logical model
|
|
2
|
+
// request owns ONE window (stall-policy.mjs PROVIDER_FIRST_OUTPUT_TIMEOUT_MS)
|
|
3
|
+
// from its start, whatever attempts serve it: a provider retry, a loop replay
|
|
4
|
+
// or a route fallback opens a new stream for the same request and inherits
|
|
5
|
+
// its absolute deadline and whether it already produced output (a stream for
|
|
6
|
+
// a request that had output runs under the semantic idle from its opening).
|
|
7
|
+
// Only a new logical request (runtime-liveness) starts a new window.
|
|
8
|
+
//
|
|
9
|
+
// A stream joins the session's request only when it reports its progress
|
|
10
|
+
// into it (the loop's onStreamDelta). Forks of the request — a /btw side
|
|
11
|
+
// question, a cache warm, a compaction summary — carry the session id without
|
|
12
|
+
// that callback and keep a window of their own.
|
|
13
|
+
//
|
|
14
|
+
// The window is shared by all four transports: the timer-driven ones (WS,
|
|
15
|
+
// HTTP/SSE) use arm/noteOutput/clear, the iterator-driven compat streams use
|
|
16
|
+
// idleWatch.
|
|
17
|
+
import { getLogicalModelRequest } from '../../session/manager/runtime-liveness.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} input
|
|
21
|
+
* @param {string|null} [input.sessionId]
|
|
22
|
+
* @param {Function} [input.onStreamDelta] the stream's progress callback
|
|
23
|
+
* @param {number} input.timeoutMs the window length
|
|
24
|
+
*/
|
|
25
|
+
export function streamFirstOutputWindow({ sessionId, onStreamDelta, timeoutMs }) {
|
|
26
|
+
const request = typeof onStreamDelta === 'function' ? getLogicalModelRequest(sessionId) : null;
|
|
27
|
+
const deadlineAt = (request?.startedAt || Date.now()) + timeoutMs;
|
|
28
|
+
let timer = null;
|
|
29
|
+
const clear = () => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
};
|
|
33
|
+
const window = {
|
|
34
|
+
timeoutMs,
|
|
35
|
+
/** Whether the request (or, after noteOutput, this stream) has produced semantic output. */
|
|
36
|
+
outputSeen: request?.outputSeen === true,
|
|
37
|
+
/** Calls `onExpire` at the request's deadline. */
|
|
38
|
+
arm(onExpire) {
|
|
39
|
+
clear();
|
|
40
|
+
timer = setTimeout(onExpire, Math.max(0, deadlineAt - Date.now()));
|
|
41
|
+
timer.unref?.();
|
|
42
|
+
},
|
|
43
|
+
clear,
|
|
44
|
+
/** The stream's semantic output: ends the window. */
|
|
45
|
+
noteOutput() {
|
|
46
|
+
window.outputSeen = true;
|
|
47
|
+
clear();
|
|
48
|
+
},
|
|
49
|
+
/** The `nextAsyncWithWatchdog` idle bounds: the semantic idle once output
|
|
50
|
+
* started (`semanticDeadlineAt` set), the window before. */
|
|
51
|
+
idleWatch(semanticDeadlineAt, idleMs) {
|
|
52
|
+
return semanticDeadlineAt > 0
|
|
53
|
+
? { idleMs, idleDeadlineAt: semanticDeadlineAt }
|
|
54
|
+
: { idleMs: timeoutMs, idleDeadlineAt: deadlineAt };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
return window;
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { listenOnFetchSafePort } from '../../../../shared/fetch-safe-listen.mjs';
|
|
2
|
+
|
|
3
|
+
// Port 0 takes an OS-assigned port the browser can be redirected to.
|
|
4
|
+
export function listenOn(server, port, host) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
server.once('error', reject);
|
|
7
|
+
const listening = () => {
|
|
8
|
+
server.off('error', reject);
|
|
9
|
+
resolve();
|
|
10
|
+
};
|
|
11
|
+
if (port === 0) listenOnFetchSafePort(server, host, listening);
|
|
12
|
+
else server.listen(port, host, listening);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// X25519 public key we send in the authorize URL; the reply (loopback redirect
|
|
3
3
|
// or a pasted code) carries `u` = ephemeralPub(32) | nonce(12) | ciphertext | tag(16).
|
|
4
4
|
import { createServer } from 'node:http';
|
|
5
|
+
import { listenOn } from './lib/listen-on.mjs';
|
|
5
6
|
import { lookup } from 'node:dns/promises';
|
|
6
7
|
import { createDecipheriv, createHash, createPublicKey, diffieHellman, generateKeyPairSync } from 'node:crypto';
|
|
7
8
|
import { openLoginUrl } from './lib/open-login-url.mjs';
|
|
@@ -112,16 +113,6 @@ async function loopbackAddresses() {
|
|
|
112
113
|
return ['127.0.0.1'];
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
function listenOn(server, port, host) {
|
|
116
|
-
return new Promise((resolve, reject) => {
|
|
117
|
-
server.once('error', reject);
|
|
118
|
-
server.listen(port, host, () => {
|
|
119
|
-
server.off('error', reject);
|
|
120
|
-
resolve();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
116
|
export async function beginOAuthLogin({
|
|
126
117
|
openBrowser = true,
|
|
127
118
|
timeoutMs = LOGIN_TIMEOUT_MS,
|