mixdog 1.3.3 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +3 -2
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/agent-room-host.mjs +1 -1
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +335 -172
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* Replay stays gated by the stream-outcome contract: a stream that exposed
|
|
24
24
|
* text or dispatched a tool is never re-issued.
|
|
25
25
|
*/
|
|
26
|
+
import { noteAbandonedUsage } from '../../../shared/llm/usage-context.mjs';
|
|
26
27
|
import { readStreamOutcome } from './lib/stream-outcome.mjs';
|
|
27
28
|
import { DETERMINISTIC_KINDS, providerErrorKind, typedCodeKind } from './provider-error-kind.mjs';
|
|
28
29
|
import { midstreamRetryDelayMs } from './retry-backoff.mjs';
|
|
@@ -42,7 +43,7 @@ export const GEMINI_REQUEST_RETRY = Object.freeze({
|
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
// A server wait longer than this is a lock-out, not a retry.
|
|
45
|
-
|
|
46
|
+
const GEMINI_MAX_RETRYABLE_ADVICE_MS = 300_000;
|
|
46
47
|
|
|
47
48
|
function hasLongAdvice(err) {
|
|
48
49
|
const { kind, retryAfterMs } = providerErrorKind(err);
|
|
@@ -56,7 +57,7 @@ export function geminiEndsWithFunctionResponse(contents) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export const GEMINI_MIDSTREAM_MAX_ATTEMPTS = 4;
|
|
59
|
-
|
|
60
|
+
const GEMINI_MIDSTREAM_INITIAL_DELAY_MS = 1000;
|
|
60
61
|
|
|
61
62
|
const midstreamStepMs = (streamAttempt) => GEMINI_MIDSTREAM_INITIAL_DELAY_MS * 2 ** streamAttempt;
|
|
62
63
|
|
|
@@ -116,7 +117,14 @@ export function geminiInvalidStreamReason(response, textLeakGuard = null, { afte
|
|
|
116
117
|
* open + consume and calls `connected()` once the response opened; it resolves
|
|
117
118
|
* `{ response, textLeakGuard }`.
|
|
118
119
|
*/
|
|
119
|
-
export async function runGeminiStreamAttempts({
|
|
120
|
+
export async function runGeminiStreamAttempts({
|
|
121
|
+
run,
|
|
122
|
+
signal,
|
|
123
|
+
onRetry,
|
|
124
|
+
sleepFn,
|
|
125
|
+
afterToolResult = false,
|
|
126
|
+
failureUsage = null,
|
|
127
|
+
}) {
|
|
120
128
|
for (let streamAttempt = 0; ; streamAttempt++) {
|
|
121
129
|
let connected = false;
|
|
122
130
|
let result;
|
|
@@ -157,29 +165,60 @@ export async function runGeminiStreamAttempts({ run, signal, onRetry, sleepFn, a
|
|
|
157
165
|
await sleepWithAbort(waitMs, signal, sleepFn, 'Gemini stream retry wait aborted');
|
|
158
166
|
continue;
|
|
159
167
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
if (signal?.aborted) return result;
|
|
169
|
+
const reask = geminiInvalidStreamReask(result.response, result.textLeakGuard, {
|
|
170
|
+
afterToolResult,
|
|
171
|
+
streamAttempt,
|
|
172
|
+
failureUsage,
|
|
165
173
|
});
|
|
166
|
-
if (
|
|
167
|
-
// A spent ladder on an empty answer is a failure, not an answer; a
|
|
168
|
-
// malformed / unexpected tool call keeps its finish-reason error.
|
|
169
|
-
if (invalid !== 'NO_RESPONSE_TEXT') return result;
|
|
170
|
-
throw markProviderRecoveryExhausted(unusable, {
|
|
171
|
-
owner: 'gemini-stream',
|
|
172
|
-
attempts: GEMINI_MIDSTREAM_MAX_ATTEMPTS,
|
|
173
|
-
terminal: true,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
const waitMs = midstreamStepMs(streamAttempt);
|
|
174
|
+
if (!reask) return result;
|
|
177
175
|
onRetry?.({
|
|
178
176
|
attempt: streamAttempt + 1,
|
|
179
177
|
maxAttempts: GEMINI_MIDSTREAM_MAX_ATTEMPTS,
|
|
180
|
-
lastErr: unusable,
|
|
181
|
-
delayMs: waitMs,
|
|
178
|
+
lastErr: reask.unusable,
|
|
179
|
+
delayMs: reask.waitMs,
|
|
180
|
+
});
|
|
181
|
+
await sleepWithAbort(reask.waitMs, signal, sleepFn, 'Gemini stream retry wait aborted');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The bounded re-ask for a finished but unusable response (empty STOP,
|
|
187
|
+
* malformed / unexpected tool call), shared by every Gemini-shaped transport:
|
|
188
|
+
* `{ unusable, waitMs }` when the request should be re-issued after `waitMs`,
|
|
189
|
+
* null when the response stands (usable, or a spent ladder on a malformed
|
|
190
|
+
* call, which keeps its finish-reason error). A spent ladder on an empty
|
|
191
|
+
* answer throws: it is a failure, not an answer.
|
|
192
|
+
*
|
|
193
|
+
* The discarded response was still billed. `failureUsage` ({ model,
|
|
194
|
+
* normalize(usageMetadata) }) records it: a scheduled re-ask notes it as
|
|
195
|
+
* abandoned usage; a spent ladder that throws carries it as
|
|
196
|
+
* partialUsage/partialModel (the send's error accounting records it once).
|
|
197
|
+
*/
|
|
198
|
+
export function geminiInvalidStreamReask(
|
|
199
|
+
response,
|
|
200
|
+
textLeakGuard,
|
|
201
|
+
{ afterToolResult = false, streamAttempt = 0, failureUsage = null } = {}
|
|
202
|
+
) {
|
|
203
|
+
const invalid = geminiInvalidStreamReason(response, textLeakGuard, { afterToolResult });
|
|
204
|
+
if (!invalid) return null;
|
|
205
|
+
const unusable = Object.assign(new Error(`Gemini stream unusable: ${invalid}`), {
|
|
206
|
+
name: 'InvalidStreamError',
|
|
207
|
+
code: invalid.toLowerCase(),
|
|
208
|
+
});
|
|
209
|
+
const usage = response?.usageMetadata && failureUsage ? failureUsage.normalize(response.usageMetadata) : null;
|
|
210
|
+
if (streamAttempt + 1 >= GEMINI_MIDSTREAM_MAX_ATTEMPTS) {
|
|
211
|
+
if (invalid !== 'NO_RESPONSE_TEXT') return null;
|
|
212
|
+
if (usage) {
|
|
213
|
+
unusable.partialUsage = usage;
|
|
214
|
+
unusable.partialModel = failureUsage.model;
|
|
215
|
+
}
|
|
216
|
+
throw markProviderRecoveryExhausted(unusable, {
|
|
217
|
+
owner: 'gemini-stream',
|
|
218
|
+
attempts: GEMINI_MIDSTREAM_MAX_ATTEMPTS,
|
|
219
|
+
terminal: true,
|
|
182
220
|
});
|
|
183
|
-
await sleepWithAbort(waitMs, signal, sleepFn, 'Gemini stream retry wait aborted');
|
|
184
221
|
}
|
|
222
|
+
if (usage) noteAbandonedUsage(usage, failureUsage.model);
|
|
223
|
+
return { unusable, waitMs: midstreamStepMs(streamAttempt) };
|
|
185
224
|
}
|
|
@@ -96,6 +96,7 @@ export async function streamGeminiSdkAttempt(genModel, stream, attemptSignal, on
|
|
|
96
96
|
signal: attemptSignal,
|
|
97
97
|
onStreamDelta: callbacks.onStreamDelta,
|
|
98
98
|
onTextDelta: callbacks.onTextDelta,
|
|
99
|
+
onReasoningDelta: callbacks.onReasoningDelta,
|
|
99
100
|
textLeakGuard,
|
|
100
101
|
label: 'Gemini SDK streamGenerateContent',
|
|
101
102
|
failureUsage: geminiFailureUsage(stream.opts, stream.cachedContent, stream.useModel),
|
|
@@ -51,7 +51,7 @@ const GEMINI_RPC_CODES = new Set(['UNAVAILABLE', 'DEADLINE_EXCEEDED', 'ABORTED',
|
|
|
51
51
|
* Copy a typed Gemini/gRPC status onto the error so classifyError can retry
|
|
52
52
|
* UNAVAILABLE/DEADLINE_EXCEEDED/ABORTED/INTERNAL without an HTTP status.
|
|
53
53
|
*/
|
|
54
|
-
|
|
54
|
+
function stampGeminiRpcError(err) {
|
|
55
55
|
if (!err || typeof err !== 'object') return err;
|
|
56
56
|
if (typeof err.geminiStatus === 'string' && err.geminiStatus) return err;
|
|
57
57
|
for (const field of [err.status, err.error?.status, err.cause?.status, err.cause?.geminiStatus]) {
|
|
@@ -326,16 +326,43 @@ function mergeStreamMetadata(previous, next) {
|
|
|
326
326
|
|
|
327
327
|
// A stream that ends with no finishReason is unfinished, whatever it carried
|
|
328
328
|
// (thoughts only, an empty body, even right after a tool result); only a
|
|
329
|
-
// STOP that carries no text is a completed, empty answer.
|
|
330
|
-
|
|
329
|
+
// STOP that carries no text is a completed, empty answer. The one exception
|
|
330
|
+
// is a stream that delivered at least one whole functionCall part and no
|
|
331
|
+
// explicitly unfinished one: the gateway ends such a tool-use turn without a
|
|
332
|
+
// finishReason, and every call is intact.
|
|
333
|
+
function assertGeminiStreamCompleted({ sawStreamChunk, finishReason, promptBlockReason, parts, label }) {
|
|
331
334
|
if (!sawStreamChunk) {
|
|
332
335
|
throw geminiTruncatedStreamError(`${label} truncated: empty stream`);
|
|
333
336
|
}
|
|
334
337
|
if (geminiFinishReasonKind(finishReason) === 'none' && geminiFinishReasonKind(promptBlockReason) === 'none') {
|
|
338
|
+
if (Array.isArray(parts)) {
|
|
339
|
+
const calls = parts.filter((part) => part?.functionCall);
|
|
340
|
+
if (calls.length > 0 && calls.every(isCompleteFunctionCallPart)) return;
|
|
341
|
+
}
|
|
335
342
|
throw geminiTruncatedStreamError(`${label} truncated: no finishReason`);
|
|
336
343
|
}
|
|
337
344
|
}
|
|
338
345
|
|
|
346
|
+
// A functionCall part is whole when it names the function and its arguments
|
|
347
|
+
// are not still streaming in pieces.
|
|
348
|
+
function isIncompleteFunctionCallPart(part) {
|
|
349
|
+
const call = part?.functionCall;
|
|
350
|
+
if (!call) return false;
|
|
351
|
+
return (
|
|
352
|
+
call.willContinue === true ||
|
|
353
|
+
call.will_continue === true ||
|
|
354
|
+
call.partialArgs !== undefined ||
|
|
355
|
+
call.partial_args !== undefined
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isCompleteFunctionCallPart(part) {
|
|
360
|
+
const call = part?.functionCall;
|
|
361
|
+
if (!call || typeof call.name !== 'string' || call.name === '') return false;
|
|
362
|
+
if (isIncompleteFunctionCallPart(part)) return false;
|
|
363
|
+
return call.args === undefined || (call.args !== null && typeof call.args === 'object' && !Array.isArray(call.args));
|
|
364
|
+
}
|
|
365
|
+
|
|
339
366
|
// Concatenate the text parts of a single streamed Gemini chunk. Used to feed
|
|
340
367
|
// the gateway live-text relay (onTextDelta) with the incremental text payload
|
|
341
368
|
// as each SSE/SDK chunk arrives. Returns '' for tool-call / thought-only /
|
|
@@ -351,6 +378,27 @@ function geminiChunkText(chunk) {
|
|
|
351
378
|
return text;
|
|
352
379
|
}
|
|
353
380
|
|
|
381
|
+
// The thought-summary text of a streamed chunk, relayed as reasoning and never
|
|
382
|
+
// mixed into the answer text.
|
|
383
|
+
function geminiChunkThoughtText(chunk) {
|
|
384
|
+
const parts = chunk?.candidates?.[0]?.content?.parts;
|
|
385
|
+
if (!Array.isArray(parts)) return '';
|
|
386
|
+
let text = '';
|
|
387
|
+
for (const p of parts) {
|
|
388
|
+
if (p && p.thought === true && typeof p.text === 'string') text += p.text;
|
|
389
|
+
}
|
|
390
|
+
return text;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function relayGeminiReasoning(chunk, onReasoningDelta) {
|
|
394
|
+
if (!onReasoningDelta) return;
|
|
395
|
+
const thought = geminiChunkThoughtText(chunk);
|
|
396
|
+
if (!thought) return;
|
|
397
|
+
try {
|
|
398
|
+
onReasoningDelta(thought);
|
|
399
|
+
} catch {}
|
|
400
|
+
}
|
|
401
|
+
|
|
354
402
|
// Returns the text actually RELAYED to the live sink ('' when it was only
|
|
355
403
|
// buffered), so callers can track visible output rather than raw stream text.
|
|
356
404
|
function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
|
|
@@ -384,6 +432,9 @@ export function createGeminiTextLeakGuard({
|
|
|
384
432
|
const _enabled = _knownTools.size > 0;
|
|
385
433
|
const _isKnownTool = (name) => _knownTools.has(name);
|
|
386
434
|
let leakBuffer = '';
|
|
435
|
+
// Markdown fence / inline-code state carried across chunks: a tag inside a
|
|
436
|
+
// code fence opened in an earlier chunk is an example, not a call.
|
|
437
|
+
let fenceState = null;
|
|
387
438
|
let relayedText = '';
|
|
388
439
|
const leakedCalls = [];
|
|
389
440
|
// With `deferToolCalls`, recovered calls wait here until the caller has seen
|
|
@@ -425,8 +476,18 @@ export function createGeminiTextLeakGuard({
|
|
|
425
476
|
const pumpLeakBuffer = (final) => {
|
|
426
477
|
if (!_enabled) return;
|
|
427
478
|
if (!leakBuffer && !final) return;
|
|
428
|
-
const {
|
|
479
|
+
const {
|
|
480
|
+
emit,
|
|
481
|
+
calls,
|
|
482
|
+
rest,
|
|
483
|
+
fenceState: nextFence,
|
|
484
|
+
} = scanLeakedToolCalls(leakBuffer, {
|
|
485
|
+
isKnownTool: _isKnownTool,
|
|
486
|
+
final,
|
|
487
|
+
fenceState,
|
|
488
|
+
});
|
|
429
489
|
leakBuffer = rest;
|
|
490
|
+
fenceState = nextFence;
|
|
430
491
|
// Only text handed to the live sink counts as relayed/visible. Without
|
|
431
492
|
// an onTextDelta sink the scrubbed text is buffered for the final
|
|
432
493
|
// result and never shown, so it must not deny a replay.
|
|
@@ -521,6 +582,7 @@ export async function consumeGeminiRestStreamResponse(
|
|
|
521
582
|
signal,
|
|
522
583
|
onStreamDelta,
|
|
523
584
|
onTextDelta,
|
|
585
|
+
onReasoningDelta = null,
|
|
524
586
|
textLeakGuard,
|
|
525
587
|
label,
|
|
526
588
|
unwrapChunk = null,
|
|
@@ -568,6 +630,7 @@ export async function consumeGeminiRestStreamResponse(
|
|
|
568
630
|
try {
|
|
569
631
|
onStreamDelta?.(geminiChunkProgressKind(parsed));
|
|
570
632
|
} catch {}
|
|
633
|
+
relayGeminiReasoning(parsed, onReasoningDelta);
|
|
571
634
|
if (onTextDelta || textLeakGuard) {
|
|
572
635
|
relayedText += relayGeminiStreamText(geminiChunkText(parsed), { onTextDelta, textLeakGuard });
|
|
573
636
|
}
|
|
@@ -665,6 +728,7 @@ export async function consumeGeminiRestStreamResponse(
|
|
|
665
728
|
sawStreamChunk,
|
|
666
729
|
finishReason,
|
|
667
730
|
promptBlockReason,
|
|
731
|
+
parts: aggregated.candidates?.[0]?.content?.parts,
|
|
668
732
|
label,
|
|
669
733
|
});
|
|
670
734
|
} catch (err) {
|
|
@@ -679,6 +743,7 @@ export async function consumeGeminiSdkStream(
|
|
|
679
743
|
signal,
|
|
680
744
|
onStreamDelta,
|
|
681
745
|
onTextDelta,
|
|
746
|
+
onReasoningDelta = null,
|
|
682
747
|
textLeakGuard,
|
|
683
748
|
label,
|
|
684
749
|
cancelGeneration,
|
|
@@ -741,6 +806,7 @@ export async function consumeGeminiSdkStream(
|
|
|
741
806
|
try {
|
|
742
807
|
onStreamDelta?.(geminiChunkProgressKind(step.value));
|
|
743
808
|
} catch {}
|
|
809
|
+
relayGeminiReasoning(step.value, onReasoningDelta);
|
|
744
810
|
if (onTextDelta || textLeakGuard) {
|
|
745
811
|
const t = geminiChunkText(step.value);
|
|
746
812
|
relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
@@ -770,6 +836,7 @@ export async function consumeGeminiSdkStream(
|
|
|
770
836
|
sawStreamChunk: reader.sawStreamChunk,
|
|
771
837
|
finishReason,
|
|
772
838
|
promptBlockReason,
|
|
839
|
+
parts: raw?.candidates?.[0]?.content?.parts,
|
|
773
840
|
label,
|
|
774
841
|
});
|
|
775
842
|
return raw;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './gemini-retry-policy.mjs';
|
|
32
32
|
import { geminiTextLeakGuardFor, streamGeminiSdkAttempt } from './gemini-sdk-request.mjs';
|
|
33
33
|
import { emitGeminiToolCalls } from './gemini-schema.mjs';
|
|
34
|
+
import { createGeminiReasoningRelay } from './gemini-reasoning-relay.mjs';
|
|
34
35
|
import { buildGeminiRequest, geminiCachedRestBody } from './gemini-request-body.mjs';
|
|
35
36
|
import {
|
|
36
37
|
geminiIncompleteError,
|
|
@@ -134,10 +135,14 @@ function geminiRetryLogger(opts, tag) {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
function geminiSendCallbacks(opts) {
|
|
138
|
+
const reasoning = createGeminiReasoningRelay(opts);
|
|
137
139
|
return {
|
|
138
140
|
onStreamDelta: typeof opts.onStreamDelta === 'function' ? opts.onStreamDelta : null,
|
|
139
141
|
onToolCall: typeof opts.onToolCall === 'function' ? opts.onToolCall : null,
|
|
140
142
|
onTextDelta: typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null,
|
|
143
|
+
onReasoningDelta: reasoning.onReasoningDelta,
|
|
144
|
+
// Thinking already shown by a dead or discarded attempt.
|
|
145
|
+
retractReasoning: reasoning.retract,
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
|
|
@@ -220,11 +225,12 @@ export class GeminiProvider {
|
|
|
220
225
|
* dispatched tool can never run twice. Returns the aggregated response, or
|
|
221
226
|
* null when the failure is ineligible or the fallback itself fails.
|
|
222
227
|
*/
|
|
223
|
-
async _recoverGeminiNonStreaming({ streamErr, signal, opts, model, generate }) {
|
|
228
|
+
async _recoverGeminiNonStreaming({ streamErr, signal, opts, model, generate, retractReasoning }) {
|
|
224
229
|
if (!canFallbackNonStreaming(streamErr, { signal })) return null;
|
|
225
230
|
noteErrorUsage(streamErr);
|
|
226
231
|
let aggregated;
|
|
227
232
|
try {
|
|
233
|
+
retractReasoning?.();
|
|
228
234
|
signalRequesting(opts);
|
|
229
235
|
aggregated = await generate(signal || undefined);
|
|
230
236
|
} catch {
|
|
@@ -549,6 +555,16 @@ export class GeminiProvider {
|
|
|
549
555
|
}
|
|
550
556
|
}
|
|
551
557
|
|
|
558
|
+
// The output limit the live model roster declared for this model, or null.
|
|
559
|
+
_rosterOutputLimit(model) {
|
|
560
|
+
try {
|
|
561
|
+
const id = geminiBareModelId(model);
|
|
562
|
+
return this._modelCache.loadSync()?.find?.((entry) => entry?.id === id)?.outputTokens ?? null;
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
552
568
|
async _doSend(messages, model, tools, sendOpts, internal = {}) {
|
|
553
569
|
const opts = sendOpts || {};
|
|
554
570
|
const signal = opts.signal || null;
|
|
@@ -556,7 +572,7 @@ export class GeminiProvider {
|
|
|
556
572
|
throwIfGeminiAborted(signal);
|
|
557
573
|
|
|
558
574
|
const useModel = model || (await ensureLatestGeminiModel(this));
|
|
559
|
-
const request = buildGeminiRequest(messages, useModel, tools, opts);
|
|
575
|
+
const request = buildGeminiRequest(messages, useModel, tools, opts, this._rosterOutputLimit(useModel));
|
|
560
576
|
signalRequesting(opts);
|
|
561
577
|
|
|
562
578
|
// Explicit cachedContents (system + tools + prior-turn transcript).
|
|
@@ -690,7 +706,9 @@ export class GeminiProvider {
|
|
|
690
706
|
sleepFn: this.config?.sleepFn,
|
|
691
707
|
onRetry: geminiRetryLogger(opts, '[gemini-rest]'),
|
|
692
708
|
afterToolResult,
|
|
709
|
+
failureUsage: geminiFailureUsage(opts, cachedContent, useModel),
|
|
693
710
|
run: async ({ signal: attemptSignal, connected }) => {
|
|
711
|
+
callbacks.retractReasoning();
|
|
694
712
|
signalRequesting(opts);
|
|
695
713
|
const res = await this._openRestStream(genUrl, body, attemptSignal);
|
|
696
714
|
connected();
|
|
@@ -699,6 +717,7 @@ export class GeminiProvider {
|
|
|
699
717
|
signal: attemptSignal,
|
|
700
718
|
onStreamDelta: callbacks.onStreamDelta,
|
|
701
719
|
onTextDelta: callbacks.onTextDelta,
|
|
720
|
+
onReasoningDelta: callbacks.onReasoningDelta,
|
|
702
721
|
textLeakGuard,
|
|
703
722
|
label: 'Gemini REST streamGenerateContent',
|
|
704
723
|
failureUsage: geminiFailureUsage(opts, cachedContent, useModel),
|
|
@@ -722,6 +741,7 @@ export class GeminiProvider {
|
|
|
722
741
|
opts,
|
|
723
742
|
model: useModel,
|
|
724
743
|
generate: (fallbackSignal) => this._restGenerateContent(useModel, apiKey, body, fallbackSignal),
|
|
744
|
+
retractReasoning: callbacks.retractReasoning,
|
|
725
745
|
});
|
|
726
746
|
if (!recovered) throw err;
|
|
727
747
|
return { response: recovered, textLeakGuard: null };
|
|
@@ -760,7 +780,9 @@ export class GeminiProvider {
|
|
|
760
780
|
sleepFn: this.config?.sleepFn,
|
|
761
781
|
onRetry: geminiRetryLogger(opts, '[gemini]'),
|
|
762
782
|
afterToolResult: geminiEndsWithFunctionResponse(contents),
|
|
783
|
+
failureUsage: geminiFailureUsage(opts, stream.cachedContent, useModel),
|
|
763
784
|
run: async ({ signal: attemptSignal, connected }) => {
|
|
785
|
+
stream.callbacks.retractReasoning();
|
|
764
786
|
signalRequesting(opts);
|
|
765
787
|
return await streamGeminiSdkAttempt(genModel, stream, attemptSignal, connected);
|
|
766
788
|
},
|
|
@@ -775,6 +797,7 @@ export class GeminiProvider {
|
|
|
775
797
|
const result = await genModel.generateContent({ contents }, { signal: fallbackSignal });
|
|
776
798
|
return result?.response ?? null;
|
|
777
799
|
},
|
|
800
|
+
retractReasoning: stream.callbacks.retractReasoning,
|
|
778
801
|
});
|
|
779
802
|
if (!recovered) throw err;
|
|
780
803
|
return { response: recovered, textLeakGuard: null };
|
|
@@ -3,7 +3,7 @@ import { createNpmVersionSource, maxSemver } from './npm-cli-version.mjs';
|
|
|
3
3
|
// Grok CLI client version for the cli-chat-proxy version gate (HTTP 426).
|
|
4
4
|
// Effective = env override, else max(floor, learned-from-426, live npm latest
|
|
5
5
|
// of the official @xai-official/grok CLI).
|
|
6
|
-
export const GROK_CLI_VERSION_FLOOR = '1.0.
|
|
6
|
+
export const GROK_CLI_VERSION_FLOOR = '1.0.70';
|
|
7
7
|
export const GROK_CLI_NPM_PACKAGE = '@xai-official/grok';
|
|
8
8
|
|
|
9
9
|
const live = createNpmVersionSource(GROK_CLI_NPM_PACKAGE, { persistKey: 'grok-cli' });
|
|
@@ -11,12 +11,13 @@ import { reportedCostUsd } from './usage-primitives.mjs';
|
|
|
11
11
|
import { reportInvalidTool, wireInputSchema } from './claude-bridge-schema.mjs';
|
|
12
12
|
import { normalizeServerToolUse } from './anthropic-server-tool-usage.mjs';
|
|
13
13
|
import { ANTHROPIC_TOOL_CALL_IDS, withWireToolCallIds } from './tool-call-wire-ids.mjs';
|
|
14
|
+
import { supportsAnthropicTurnScopedSystemMessages } from '../../../../shared/llm/anthropic-thinking-contract.mjs';
|
|
14
15
|
export const ANTHROPIC_CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
|
|
15
16
|
const ANTHROPIC_CACHE_TTL_VOLATILE = { type: 'ephemeral' };
|
|
16
17
|
|
|
17
18
|
// Anthropic allows at most 4 cache_control breakpoints across the whole
|
|
18
19
|
// serialized request (tools + system + messages).
|
|
19
|
-
|
|
20
|
+
const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
|
|
20
21
|
|
|
21
22
|
// A block can carry a breakpoint unless it is thinking/redacted_thinking or an
|
|
22
23
|
// empty text block (both are rejected by the API).
|
|
@@ -98,6 +99,84 @@ export function appendAnthropicTailReminder(anthropicMessages, reminder) {
|
|
|
98
99
|
return anthropicMessages;
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// Turn-scoped tail reminders (see tail-reminder-split.mjs). The capability:
|
|
103
|
+
// the first-party Messages routes (no gateway base URL, beta headers allowed)
|
|
104
|
+
// on a model that takes clear_at system messages. Everything else keeps the
|
|
105
|
+
// legacy appendAnthropicTailReminder layout.
|
|
106
|
+
// Server-side fallback needs no exclusion: live-checked on the OAuth route,
|
|
107
|
+
// a trailing clear_at message is accepted with `fallbacks: "default"` (Opus
|
|
108
|
+
// 5.5, Sonnet 5.5) and with explicit lists naming Sonnet 5 or Opus 4.8, and
|
|
109
|
+
// fallback runs only on a safety-classifier refusal.
|
|
110
|
+
export function anthropicTurnScopedReminders(provider, model, config = {}) {
|
|
111
|
+
return (
|
|
112
|
+
ANTHROPIC_NATIVE_PROVIDERS.has(provider) &&
|
|
113
|
+
config?.disableBetaHeaders !== true &&
|
|
114
|
+
(!config?.baseURL || /^https:\/\/api\.anthropic\.com(?:\/|$)/.test(config.baseURL)) &&
|
|
115
|
+
supportsAnthropicTurnScopedSystemMessages(model)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The stored reminder of a lowered assistant turn (toAnthropicMessages). A
|
|
120
|
+
// symbol key: JSON serialization never puts it on the wire.
|
|
121
|
+
const TURN_REMINDER = Symbol('turnReminder');
|
|
122
|
+
|
|
123
|
+
function assistantWireMessage(message, content) {
|
|
124
|
+
const wire = { role: 'assistant', content };
|
|
125
|
+
if (typeof message.turnReminder === 'string' && message.turnReminder) wire[TURN_REMINDER] = message.turnReminder;
|
|
126
|
+
return wire;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Derived only from the stored text, so a replay is byte-identical to the
|
|
130
|
+
// request that first sent it. Text only, never a cache_control (both 400).
|
|
131
|
+
function turnReminderMessage(text) {
|
|
132
|
+
return { role: 'system', clear_at: 'next_user_message', content: [{ type: 'text', text }] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function hasAnthropicTurnScopedMessages(messages) {
|
|
136
|
+
return Array.isArray(messages) && messages.some((m) => m?.role === 'system' && m.clear_at);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A turn-scoped message must follow a user turn; an effort control (empty
|
|
140
|
+
// system message, lowerAnthropicEffortHistory) may sit between them. Creation
|
|
141
|
+
// (appendAnthropicTurnReminder) and replay (withAnthropicTurnReminders) both
|
|
142
|
+
// decide by this one rule on the lowered array, so a turn whose request carried
|
|
143
|
+
// no reminder never gets one on replay.
|
|
144
|
+
function followsUserTurn(messages) {
|
|
145
|
+
let index = messages.length - 1;
|
|
146
|
+
const last = messages[index];
|
|
147
|
+
if (last?.role === 'system' && Array.isArray(last.content) && last.content.length === 0) index--;
|
|
148
|
+
return messages[index]?.role === 'user';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The lowered history with each stored reminder re-sent right before the
|
|
152
|
+
* assistant turn generated under it (after any effort control), exactly where
|
|
153
|
+
* that turn's own request carried it as its last message. String contents
|
|
154
|
+
* become one text block, so a cache marker (which turns a string into a block
|
|
155
|
+
* list on the turn it lands) never changes a message's shape between
|
|
156
|
+
* requests: the wire stays append-only. */
|
|
157
|
+
export function withAnthropicTurnReminders(anthropicMessages) {
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const message of anthropicMessages) {
|
|
160
|
+
const text = message?.role === 'assistant' ? message[TURN_REMINDER] : '';
|
|
161
|
+
if (text && followsUserTurn(out)) out.push(turnReminderMessage(text));
|
|
162
|
+
out.push(
|
|
163
|
+
typeof message?.content === 'string' && message.content.trim()
|
|
164
|
+
? { ...message, content: [{ type: 'text', text: message.content }] }
|
|
165
|
+
: message
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The current reminder as a turn-scoped system message ending the request.
|
|
172
|
+
* A tail that is not a user turn (an assistant continuation) cannot take one
|
|
173
|
+
* and gets no reminder at all: anything else would be missing from the next
|
|
174
|
+
* request, which replays that turn without one. */
|
|
175
|
+
export function appendAnthropicTurnReminder(anthropicMessages, reminder) {
|
|
176
|
+
if (reminder && followsUserTurn(anthropicMessages)) anthropicMessages.push(turnReminderMessage(reminder));
|
|
177
|
+
return anthropicMessages;
|
|
178
|
+
}
|
|
179
|
+
|
|
101
180
|
export function resolveAnthropicCacheTtls(opts) {
|
|
102
181
|
const strategy = opts?.cacheStrategy || {};
|
|
103
182
|
const pick = (layer, fallback) => {
|
|
@@ -278,7 +357,8 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
278
357
|
if (tb && typeof tb === 'object') content.push(tb);
|
|
279
358
|
}
|
|
280
359
|
}
|
|
281
|
-
|
|
360
|
+
// A blank text block is a hard 400 on every request that replays it.
|
|
361
|
+
if (!isBlankText(m.content)) content.push({ type: 'text', text: m.content });
|
|
282
362
|
for (const tc of m.toolCalls || []) {
|
|
283
363
|
content.push({
|
|
284
364
|
type: 'tool_use',
|
|
@@ -288,7 +368,7 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
288
368
|
});
|
|
289
369
|
}
|
|
290
370
|
}
|
|
291
|
-
result.push(
|
|
371
|
+
result.push(assistantWireMessage(m, content));
|
|
292
372
|
continue;
|
|
293
373
|
}
|
|
294
374
|
if (m.role === 'tool') {
|
|
@@ -346,11 +426,20 @@ export function toAnthropicMessages(transcript, availableTools, provider = '') {
|
|
|
346
426
|
) {
|
|
347
427
|
continue;
|
|
348
428
|
}
|
|
349
|
-
|
|
429
|
+
// An assistant turn with no text and no blocks (a refused or empty final
|
|
430
|
+
// kept in the transcript for diagnosis) has nothing to replay; sending it
|
|
431
|
+
// would be a blank text block the API rejects on every later request.
|
|
432
|
+
if (m.role === 'assistant' && isBlankText(m.content)) continue;
|
|
433
|
+
const content = normalizeContentForAnthropic(m.content);
|
|
434
|
+
result.push(m.role === 'assistant' ? assistantWireMessage(m, content) : { role: m.role, content });
|
|
350
435
|
}
|
|
351
436
|
return sanitizeAnthropicContentPairs(result);
|
|
352
437
|
}
|
|
353
438
|
|
|
439
|
+
function isBlankText(content) {
|
|
440
|
+
return content == null || (typeof content === 'string' && !content.trim());
|
|
441
|
+
}
|
|
442
|
+
|
|
354
443
|
export function clampAnthropicThinkingBudget(value, maxTokens) {
|
|
355
444
|
const desired = Math.floor(Number(value));
|
|
356
445
|
const max = Math.floor(Number(maxTokens));
|
|
@@ -702,9 +791,11 @@ export function applyAnthropicCacheMarkers(
|
|
|
702
791
|
|
|
703
792
|
// The breakpoint goes on the last message that has an eligible block: a
|
|
704
793
|
// trailing effort control (empty system message) or a thinking-only tail has
|
|
705
|
-
// nothing to mark
|
|
794
|
+
// nothing to mark, and a turn-scoped system message rejects cache_control.
|
|
795
|
+
// -1 when no message does.
|
|
706
796
|
function lastMarkableMessageIndex(messages) {
|
|
707
797
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
798
|
+
if (messages[index].role === 'system') continue;
|
|
708
799
|
if (lastEligibleBlockIndex(messages[index].content) >= 0) return index;
|
|
709
800
|
}
|
|
710
801
|
return -1;
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
// The tail reminder (session/tail-reminder-rules.mjs) rides the outgoing request
|
|
2
|
-
// as its last message, marked meta.source 'tail-reminder'.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// as its last message, marked meta.source 'tail-reminder'. A provider builder
|
|
3
|
+
// that keeps per-message state (cache breakpoints, message counts, continuation
|
|
4
|
+
// indexes, tool-loop closure) lifts it off first with splitTailReminder, builds
|
|
5
|
+
// from the real history, and then places it in one of two modes:
|
|
6
|
+
//
|
|
7
|
+
// - Legacy (default, every provider): the reminder is the final wire element
|
|
8
|
+
// (Anthropic: a text block on the last user message) and is never sent again,
|
|
9
|
+
// so the next request's history no longer holds it. That rewrites what came
|
|
10
|
+
// before the assistant turn it produced; Anthropic then drops that turn's
|
|
11
|
+
// thinking (prefix_binding_mismatch).
|
|
12
|
+
// - Turn-scoped (append-only): the request ends with a turn-scoped system
|
|
13
|
+
// message ({role:'system', clear_at:'next_user_message'}), and every later
|
|
14
|
+
// request re-sends it byte-identically right before the assistant turn it
|
|
15
|
+
// produced. send-with-recovery stores the reminder on that assistant message
|
|
16
|
+
// (`turnReminder`, all providers), so the wire only ever grows.
|
|
17
|
+
//
|
|
18
|
+
// Opting in is per provider builder: decide with a capability predicate
|
|
19
|
+
// (Anthropic: anthropicTurnScopedReminders in anthropic-request-utils.mjs —
|
|
20
|
+
// first-party Messages routes on a supported model),
|
|
21
|
+
// re-emit stored `turnReminder`s from the lowered history and append the
|
|
22
|
+
// current one in the same shape, both by one placement rule: a request whose
|
|
23
|
+
// tail cannot take the message gets no reminder, and replay skips that turn. Builders that have not opted in ignore `turnReminder` and keep
|
|
24
|
+
// the legacy layout; a stored reminder never reaches their wire.
|
|
7
25
|
export const TAIL_REMINDER_META = 'tail-reminder';
|
|
8
26
|
|
|
9
27
|
export function splitTailReminder(messages) {
|
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
5
|
import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
|
|
6
6
|
import { cursorTokenExpiry } from './cursor-auth.mjs';
|
|
7
|
+
import { describeDevinOAuthCredentials } from './devin-auth.mjs';
|
|
7
8
|
import { isValidMuseCredentialDocument } from './muse-code-oauth-tokens.mjs';
|
|
8
9
|
import { isValidOpenCodeGoCredentialDocument } from './opencode-go-oauth-tokens.mjs';
|
|
9
10
|
import { developerOptionEnabled, developerOptionForProvider } from '../../../shared/developer-options.mjs';
|
|
@@ -273,6 +274,21 @@ function cursorOAuthState() {
|
|
|
273
274
|
);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
// Account-scoped like the token store: the selected account's file wins. Expiry
|
|
278
|
+
// comes from describeDevinOAuthCredentials (no refresh is attempted here); an
|
|
279
|
+
// expired access token still counts when a refresh token can renew it.
|
|
280
|
+
function devinOAuthState() {
|
|
281
|
+
const paths = [
|
|
282
|
+
boundProviderAuthPath('devin-oauth') ||
|
|
283
|
+
process.env.DEVIN_OAUTH_CREDENTIALS_PATH ||
|
|
284
|
+
join(resolvePluginData(), 'devin-oauth.json'),
|
|
285
|
+
];
|
|
286
|
+
return memoProbe('devin-oauth', paths, () => {
|
|
287
|
+
const info = describeDevinOAuthCredentials();
|
|
288
|
+
return info?.authenticated && (info.usable || info.refreshable) ? 'present' : 'absent';
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
276
292
|
const OAUTH_PROBE_STATES = new Map([
|
|
277
293
|
['anthropic-oauth', anthropicOAuthState],
|
|
278
294
|
['openai-oauth', openAIOAuthState],
|
|
@@ -281,6 +297,7 @@ const OAUTH_PROBE_STATES = new Map([
|
|
|
281
297
|
['command-code-oauth', commandCodeOAuthState],
|
|
282
298
|
['mimo-oauth', mimoOAuthState],
|
|
283
299
|
['cursor-oauth', cursorOAuthState],
|
|
300
|
+
['devin-oauth', devinOAuthState],
|
|
284
301
|
['antigravity-oauth', antigravityOAuthState],
|
|
285
302
|
['opencode-go-oauth', openCodeGoOAuthState],
|
|
286
303
|
]);
|