feat(ai,coding-agent): added Anthropic fast mode with auto-fallback
Wires `speed: "fast"` and the `fast-mode-2026-02-01` beta into the Anthropic provider, plumbs a matching `speed` option through `SimpleStreamOptions` and the Agent, and teaches `/fast` to dispatch on the active model's API (Anthropic -> speed=fast, OpenAI -> existing serviceTier=priority path). Server is the authority on which models support fast mode. When the server rejects an unsupported model, the provider mirrors the strict-tools fallback: drops the field, retries the same turn transparently, persists the disable via `providerSessionState`, and surfaces the action through the new `AssistantMessage.disabledFeatures` marker so the session can sync the toggle off and warn the user.
This commit is contained in:
@@ -1578,6 +1578,14 @@ export class AgentSession {
|
||||
if (event.message.role === "assistant") {
|
||||
this.#lastAssistantMessage = event.message;
|
||||
const assistantMsg = event.message as AssistantMessage;
|
||||
if (assistantMsg.disabledFeatures?.includes("anthropic.fast_mode") && this.speed === "fast") {
|
||||
this.setSpeed(undefined);
|
||||
this.emitNotice(
|
||||
"warning",
|
||||
"Fast mode rejected for this model; retried without it. Fast mode is now off.",
|
||||
"anthropic.fast_mode",
|
||||
);
|
||||
}
|
||||
// Resolve TTSR resume gate before checking for new deferred injections.
|
||||
// Gate on #ttsrAbortPending, not stopReason: a non-TTSR abort (e.g. streaming
|
||||
// edit) also produces stopReason === "aborted" but has no continuation coming.
|
||||
@@ -2761,6 +2769,10 @@ export class AgentSession {
|
||||
return this.agent.serviceTier;
|
||||
}
|
||||
|
||||
get speed(): "fast" | "standard" | undefined {
|
||||
return this.agent.speed;
|
||||
}
|
||||
|
||||
/** Whether agent is currently streaming a response */
|
||||
get isStreaming(): boolean {
|
||||
return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
|
||||
@@ -5111,6 +5123,9 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
isFastModeEnabled(): boolean {
|
||||
if (this.model?.api === "anthropic-messages") {
|
||||
return this.speed === "fast";
|
||||
}
|
||||
return this.serviceTier === "priority";
|
||||
}
|
||||
|
||||
@@ -5120,7 +5135,17 @@ export class AgentSession {
|
||||
this.sessionManager.appendServiceTierChange(serviceTier ?? null);
|
||||
}
|
||||
|
||||
setSpeed(speed: "fast" | "standard" | undefined): void {
|
||||
if (this.speed === speed) return;
|
||||
this.agent.speed = speed;
|
||||
this.sessionManager.appendSpeedChange(speed ?? null);
|
||||
}
|
||||
|
||||
setFastMode(enabled: boolean): void {
|
||||
if (this.model?.api === "anthropic-messages") {
|
||||
this.setSpeed(enabled ? "fast" : undefined);
|
||||
return;
|
||||
}
|
||||
this.setServiceTier(enabled ? "priority" : undefined);
|
||||
}
|
||||
|
||||
@@ -7582,6 +7607,7 @@ export class AgentSession {
|
||||
reasoning: toReasoningEffort(this.thinkingLevel),
|
||||
hideThinkingSummary: this.agent.hideThinkingSummary,
|
||||
serviceTier: this.serviceTier,
|
||||
speed: this.speed,
|
||||
signal: args.signal,
|
||||
toolChoice: "none",
|
||||
},
|
||||
@@ -7821,6 +7847,7 @@ export class AgentSession {
|
||||
const hasServiceTierEntry = this.sessionManager
|
||||
.getBranch()
|
||||
.some(entry => entry.type === "service_tier_change");
|
||||
const hasSpeedEntry = this.sessionManager.getBranch().some(entry => entry.type === "speed_change");
|
||||
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
||||
const configuredServiceTier = this.settings.get("serviceTier");
|
||||
const nextThinkingLevel = resolveThinkingLevelForModel(
|
||||
@@ -7834,6 +7861,7 @@ export class AgentSession {
|
||||
: configuredServiceTier === "none"
|
||||
? undefined
|
||||
: configuredServiceTier;
|
||||
this.agent.speed = hasSpeedEntry ? sessionContext.speed : undefined;
|
||||
|
||||
if (switchingToDifferentSession) {
|
||||
this.#resetHindsightConversationTrackingIfHindsight();
|
||||
|
||||
@@ -103,6 +103,11 @@ export interface ServiceTierChangeEntry extends SessionEntryBase {
|
||||
serviceTier: ServiceTier | null;
|
||||
}
|
||||
|
||||
export interface SpeedChangeEntry extends SessionEntryBase {
|
||||
type: "speed_change";
|
||||
speed: "fast" | "standard" | null;
|
||||
}
|
||||
|
||||
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
||||
type: "compaction";
|
||||
summary: string;
|
||||
@@ -214,6 +219,7 @@ export type SessionEntry =
|
||||
| ThinkingLevelChangeEntry
|
||||
| ModelChangeEntry
|
||||
| ServiceTierChangeEntry
|
||||
| SpeedChangeEntry
|
||||
| CompactionEntry
|
||||
| BranchSummaryEntry
|
||||
| CustomEntry
|
||||
@@ -239,6 +245,7 @@ export interface SessionContext {
|
||||
messages: AgentMessage[];
|
||||
thinkingLevel?: string;
|
||||
serviceTier?: ServiceTier;
|
||||
speed?: "fast" | "standard";
|
||||
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
||||
models: Record<string, string>;
|
||||
/** Names of TTSR rules that have been injected this session */
|
||||
@@ -524,6 +531,7 @@ export function buildSessionContext(
|
||||
messages: [],
|
||||
thinkingLevel: "off",
|
||||
serviceTier: undefined,
|
||||
speed: undefined,
|
||||
models: {},
|
||||
injectedTtsrRules: [],
|
||||
selectedMCPToolNames: [],
|
||||
@@ -544,6 +552,7 @@ export function buildSessionContext(
|
||||
messages: [],
|
||||
thinkingLevel: "off",
|
||||
serviceTier: undefined,
|
||||
speed: undefined,
|
||||
models: {},
|
||||
injectedTtsrRules: [],
|
||||
selectedMCPToolNames: [],
|
||||
@@ -563,6 +572,7 @@ export function buildSessionContext(
|
||||
// Extract settings and find compaction
|
||||
let thinkingLevel: string | undefined = "off";
|
||||
let serviceTier: ServiceTier | undefined;
|
||||
let speed: "fast" | "standard" | undefined;
|
||||
const models: Record<string, string> = {};
|
||||
let compaction: CompactionEntry | null = null;
|
||||
const injectedTtsrRulesSet = new Set<string>();
|
||||
@@ -593,6 +603,8 @@ export function buildSessionContext(
|
||||
}
|
||||
} else if (entry.type === "service_tier_change") {
|
||||
serviceTier = entry.serviceTier ?? undefined;
|
||||
} else if (entry.type === "speed_change") {
|
||||
speed = entry.speed ?? undefined;
|
||||
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
||||
// Legacy fallback: infer default model from assistant messages only
|
||||
// when no explicit `model_change` (role=default) entry has been
|
||||
@@ -705,6 +717,7 @@ export function buildSessionContext(
|
||||
messages,
|
||||
thinkingLevel,
|
||||
serviceTier,
|
||||
speed,
|
||||
models,
|
||||
injectedTtsrRules,
|
||||
selectedMCPToolNames,
|
||||
@@ -2564,6 +2577,18 @@ export class SessionManager {
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
appendSpeedChange(speed: "fast" | "standard" | null): string {
|
||||
const entry: SpeedChangeEntry = {
|
||||
type: "speed_change",
|
||||
id: generateId(this.#byId),
|
||||
parentId: this.#leafId,
|
||||
timestamp: new Date().toISOString(),
|
||||
speed,
|
||||
};
|
||||
this.#appendEntry(entry);
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
/** Append a mode change as child of current leaf, then advance leaf. Returns entry id. */
|
||||
appendModeChange(mode: string, data?: Record<string, unknown>): string {
|
||||
const entry: ModeChangeEntry = {
|
||||
|
||||
Reference in New Issue
Block a user