mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-06 02:47:56 +00:00
feat: OpenAI provider/types/UI updates; provider state persistence
This commit is contained in:
parent
3ee607202e
commit
b41762e317
16 changed files with 1596 additions and 2614 deletions
|
|
@ -4,7 +4,7 @@ import { z } from "zod"
|
||||||
* ReasoningEffort
|
* ReasoningEffort
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const reasoningEfforts = ["low", "medium", "high"] as const
|
export const reasoningEfforts = ["minimal", "low", "medium", "high"] as const
|
||||||
|
|
||||||
export const reasoningEffortsSchema = z.enum(reasoningEfforts)
|
export const reasoningEffortsSchema = z.enum(reasoningEfforts)
|
||||||
|
|
||||||
|
|
@ -44,11 +44,19 @@ export const modelInfoSchema = z.object({
|
||||||
supportsImages: z.boolean().optional(),
|
supportsImages: z.boolean().optional(),
|
||||||
supportsComputerUse: z.boolean().optional(),
|
supportsComputerUse: z.boolean().optional(),
|
||||||
supportsPromptCache: z.boolean(),
|
supportsPromptCache: z.boolean(),
|
||||||
|
// Whether this model supports temperature. Some Responses models (e.g. o-series) do not.
|
||||||
|
supportsTemperature: z.boolean().optional(),
|
||||||
// Capability flag to indicate whether the model supports an output verbosity parameter
|
// Capability flag to indicate whether the model supports an output verbosity parameter
|
||||||
supportsVerbosity: z.boolean().optional(),
|
supportsVerbosity: z.boolean().optional(),
|
||||||
supportsReasoningBudget: z.boolean().optional(),
|
supportsReasoningBudget: z.boolean().optional(),
|
||||||
requiredReasoningBudget: z.boolean().optional(),
|
requiredReasoningBudget: z.boolean().optional(),
|
||||||
supportsReasoningEffort: z.boolean().optional(),
|
supportsReasoningEffort: z.boolean().optional(),
|
||||||
|
// Whether this model supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: z.boolean().optional(),
|
||||||
|
// The role to use for the system prompt ('system' or 'developer')
|
||||||
|
systemPromptRole: z.enum(["system", "developer"]).optional(),
|
||||||
|
// The default temperature for the model
|
||||||
|
defaultTemperature: z.number().optional(),
|
||||||
supportedParameters: z.array(modelParametersSchema).optional(),
|
supportedParameters: z.array(modelParametersSchema).optional(),
|
||||||
inputPrice: z.number().optional(),
|
inputPrice: z.number().optional(),
|
||||||
outputPrice: z.number().optional(),
|
outputPrice: z.number().optional(),
|
||||||
|
|
|
||||||
22
packages/types/src/providers/__tests__/openai.models.spec.ts
Normal file
22
packages/types/src/providers/__tests__/openai.models.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { describe, it, expect } from "vitest"
|
||||||
|
import { openAiNativeModels } from "../openai.js"
|
||||||
|
|
||||||
|
describe("openAiNativeModels temperature invariants", () => {
|
||||||
|
it("models with supportsTemperature === false must not specify defaultTemperature", () => {
|
||||||
|
for (const [id, info] of Object.entries(openAiNativeModels)) {
|
||||||
|
if ((info as any).supportsTemperature === false) {
|
||||||
|
expect((info as any).defaultTemperature).toBeUndefined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("gpt-5 family models must have supportsTemperature: false and no defaultTemperature", () => {
|
||||||
|
const gpt5Ids = ["gpt-5-2025-08-07", "gpt-5-mini-2025-08-07", "gpt-5-nano-2025-08-07"]
|
||||||
|
for (const id of gpt5Ids) {
|
||||||
|
const info = (openAiNativeModels as any)[id]
|
||||||
|
expect(info).toBeDefined()
|
||||||
|
expect(info.supportsTemperature).toBe(false)
|
||||||
|
expect(info.defaultTemperature).toBeUndefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -3,7 +3,7 @@ import type { ModelInfo } from "../model.js"
|
||||||
// https://openai.com/api/pricing/
|
// https://openai.com/api/pricing/
|
||||||
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
|
export type OpenAiNativeModelId = keyof typeof openAiNativeModels
|
||||||
|
|
||||||
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07"
|
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5"
|
||||||
|
|
||||||
export const openAiNativeModels = {
|
export const openAiNativeModels = {
|
||||||
"gpt-5-2025-08-07": {
|
"gpt-5-2025-08-07": {
|
||||||
|
|
@ -19,6 +19,28 @@ export const openAiNativeModels = {
|
||||||
description: "GPT-5: The best model for coding and agentic tasks across domains",
|
description: "GPT-5: The best model for coding and agentic tasks across domains",
|
||||||
// supportsVerbosity is a new capability; ensure ModelInfo includes it
|
// supportsVerbosity is a new capability; ensure ModelInfo includes it
|
||||||
supportsVerbosity: true,
|
supportsVerbosity: true,
|
||||||
|
// GPT-5 supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: true,
|
||||||
|
systemPromptRole: "developer",
|
||||||
|
supportsTemperature: false,
|
||||||
|
},
|
||||||
|
"gpt-5": {
|
||||||
|
maxTokens: 128000,
|
||||||
|
contextWindow: 400000,
|
||||||
|
supportsImages: true,
|
||||||
|
supportsPromptCache: true,
|
||||||
|
supportsReasoningEffort: true,
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
inputPrice: 1.25,
|
||||||
|
outputPrice: 10.0,
|
||||||
|
cacheReadsPrice: 0.13,
|
||||||
|
description: "GPT-5: The best model for coding and agentic tasks across domains",
|
||||||
|
// supportsVerbosity is a new capability; ensure ModelInfo includes it
|
||||||
|
supportsVerbosity: true,
|
||||||
|
// GPT-5 supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: true,
|
||||||
|
systemPromptRole: "developer",
|
||||||
|
supportsTemperature: false,
|
||||||
},
|
},
|
||||||
"gpt-5-mini-2025-08-07": {
|
"gpt-5-mini-2025-08-07": {
|
||||||
maxTokens: 128000,
|
maxTokens: 128000,
|
||||||
|
|
@ -32,6 +54,27 @@ export const openAiNativeModels = {
|
||||||
cacheReadsPrice: 0.03,
|
cacheReadsPrice: 0.03,
|
||||||
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
|
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
|
||||||
supportsVerbosity: true,
|
supportsVerbosity: true,
|
||||||
|
// GPT-5 supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: true,
|
||||||
|
systemPromptRole: "developer",
|
||||||
|
supportsTemperature: false,
|
||||||
|
},
|
||||||
|
"gpt-5-mini": {
|
||||||
|
maxTokens: 128000,
|
||||||
|
contextWindow: 400000,
|
||||||
|
supportsImages: true,
|
||||||
|
supportsPromptCache: true,
|
||||||
|
supportsReasoningEffort: true,
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
inputPrice: 0.25,
|
||||||
|
outputPrice: 2.0,
|
||||||
|
cacheReadsPrice: 0.03,
|
||||||
|
description: "GPT-5 Mini: A faster, more cost-efficient version of GPT-5 for well-defined tasks",
|
||||||
|
supportsVerbosity: true,
|
||||||
|
// GPT-5 supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: true,
|
||||||
|
systemPromptRole: "developer",
|
||||||
|
supportsTemperature: false,
|
||||||
},
|
},
|
||||||
"gpt-5-nano-2025-08-07": {
|
"gpt-5-nano-2025-08-07": {
|
||||||
maxTokens: 128000,
|
maxTokens: 128000,
|
||||||
|
|
@ -45,6 +88,27 @@ export const openAiNativeModels = {
|
||||||
cacheReadsPrice: 0.01,
|
cacheReadsPrice: 0.01,
|
||||||
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
|
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
|
||||||
supportsVerbosity: true,
|
supportsVerbosity: true,
|
||||||
|
// GPT-5 supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: true,
|
||||||
|
systemPromptRole: "developer",
|
||||||
|
supportsTemperature: false,
|
||||||
|
},
|
||||||
|
"gpt-5-nano": {
|
||||||
|
maxTokens: 128000,
|
||||||
|
contextWindow: 400000,
|
||||||
|
supportsImages: true,
|
||||||
|
supportsPromptCache: true,
|
||||||
|
supportsReasoningEffort: true,
|
||||||
|
reasoningEffort: "medium",
|
||||||
|
inputPrice: 0.05,
|
||||||
|
outputPrice: 0.4,
|
||||||
|
cacheReadsPrice: 0.01,
|
||||||
|
description: "GPT-5 Nano: Fastest, most cost-efficient version of GPT-5",
|
||||||
|
supportsVerbosity: true,
|
||||||
|
// GPT-5 supports Responses API reasoning summaries
|
||||||
|
supportsReasoningSummary: true,
|
||||||
|
systemPromptRole: "developer",
|
||||||
|
supportsTemperature: false,
|
||||||
},
|
},
|
||||||
"gpt-4.1": {
|
"gpt-4.1": {
|
||||||
maxTokens: 32_768,
|
maxTokens: 32_768,
|
||||||
|
|
@ -54,6 +118,9 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 2,
|
inputPrice: 2,
|
||||||
outputPrice: 8,
|
outputPrice: 8,
|
||||||
cacheReadsPrice: 0.5,
|
cacheReadsPrice: 0.5,
|
||||||
|
systemPromptRole: "system",
|
||||||
|
defaultTemperature: 0,
|
||||||
|
supportsTemperature: true,
|
||||||
},
|
},
|
||||||
"gpt-4.1-mini": {
|
"gpt-4.1-mini": {
|
||||||
maxTokens: 32_768,
|
maxTokens: 32_768,
|
||||||
|
|
@ -63,6 +130,9 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 0.4,
|
inputPrice: 0.4,
|
||||||
outputPrice: 1.6,
|
outputPrice: 1.6,
|
||||||
cacheReadsPrice: 0.1,
|
cacheReadsPrice: 0.1,
|
||||||
|
systemPromptRole: "system",
|
||||||
|
defaultTemperature: 0,
|
||||||
|
supportsTemperature: true,
|
||||||
},
|
},
|
||||||
"gpt-4.1-nano": {
|
"gpt-4.1-nano": {
|
||||||
maxTokens: 32_768,
|
maxTokens: 32_768,
|
||||||
|
|
@ -72,6 +142,9 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 0.1,
|
inputPrice: 0.1,
|
||||||
outputPrice: 0.4,
|
outputPrice: 0.4,
|
||||||
cacheReadsPrice: 0.025,
|
cacheReadsPrice: 0.025,
|
||||||
|
systemPromptRole: "system",
|
||||||
|
defaultTemperature: 0,
|
||||||
|
supportsTemperature: true,
|
||||||
},
|
},
|
||||||
o3: {
|
o3: {
|
||||||
maxTokens: 100_000,
|
maxTokens: 100_000,
|
||||||
|
|
@ -83,26 +156,8 @@ export const openAiNativeModels = {
|
||||||
cacheReadsPrice: 0.5,
|
cacheReadsPrice: 0.5,
|
||||||
supportsReasoningEffort: true,
|
supportsReasoningEffort: true,
|
||||||
reasoningEffort: "medium",
|
reasoningEffort: "medium",
|
||||||
},
|
systemPromptRole: "developer",
|
||||||
"o3-high": {
|
supportsTemperature: false,
|
||||||
maxTokens: 100_000,
|
|
||||||
contextWindow: 200_000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 2.0,
|
|
||||||
outputPrice: 8.0,
|
|
||||||
cacheReadsPrice: 0.5,
|
|
||||||
reasoningEffort: "high",
|
|
||||||
},
|
|
||||||
"o3-low": {
|
|
||||||
maxTokens: 100_000,
|
|
||||||
contextWindow: 200_000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 2.0,
|
|
||||||
outputPrice: 8.0,
|
|
||||||
cacheReadsPrice: 0.5,
|
|
||||||
reasoningEffort: "low",
|
|
||||||
},
|
},
|
||||||
"o4-mini": {
|
"o4-mini": {
|
||||||
maxTokens: 100_000,
|
maxTokens: 100_000,
|
||||||
|
|
@ -114,26 +169,8 @@ export const openAiNativeModels = {
|
||||||
cacheReadsPrice: 0.275,
|
cacheReadsPrice: 0.275,
|
||||||
supportsReasoningEffort: true,
|
supportsReasoningEffort: true,
|
||||||
reasoningEffort: "medium",
|
reasoningEffort: "medium",
|
||||||
},
|
systemPromptRole: "developer",
|
||||||
"o4-mini-high": {
|
supportsTemperature: false,
|
||||||
maxTokens: 100_000,
|
|
||||||
contextWindow: 200_000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 1.1,
|
|
||||||
outputPrice: 4.4,
|
|
||||||
cacheReadsPrice: 0.275,
|
|
||||||
reasoningEffort: "high",
|
|
||||||
},
|
|
||||||
"o4-mini-low": {
|
|
||||||
maxTokens: 100_000,
|
|
||||||
contextWindow: 200_000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 1.1,
|
|
||||||
outputPrice: 4.4,
|
|
||||||
cacheReadsPrice: 0.275,
|
|
||||||
reasoningEffort: "low",
|
|
||||||
},
|
},
|
||||||
"o3-mini": {
|
"o3-mini": {
|
||||||
maxTokens: 100_000,
|
maxTokens: 100_000,
|
||||||
|
|
@ -145,26 +182,8 @@ export const openAiNativeModels = {
|
||||||
cacheReadsPrice: 0.55,
|
cacheReadsPrice: 0.55,
|
||||||
supportsReasoningEffort: true,
|
supportsReasoningEffort: true,
|
||||||
reasoningEffort: "medium",
|
reasoningEffort: "medium",
|
||||||
},
|
systemPromptRole: "developer",
|
||||||
"o3-mini-high": {
|
supportsTemperature: false,
|
||||||
maxTokens: 100_000,
|
|
||||||
contextWindow: 200_000,
|
|
||||||
supportsImages: false,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 1.1,
|
|
||||||
outputPrice: 4.4,
|
|
||||||
cacheReadsPrice: 0.55,
|
|
||||||
reasoningEffort: "high",
|
|
||||||
},
|
|
||||||
"o3-mini-low": {
|
|
||||||
maxTokens: 100_000,
|
|
||||||
contextWindow: 200_000,
|
|
||||||
supportsImages: false,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 1.1,
|
|
||||||
outputPrice: 4.4,
|
|
||||||
cacheReadsPrice: 0.55,
|
|
||||||
reasoningEffort: "low",
|
|
||||||
},
|
},
|
||||||
o1: {
|
o1: {
|
||||||
maxTokens: 100_000,
|
maxTokens: 100_000,
|
||||||
|
|
@ -174,15 +193,8 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 15,
|
inputPrice: 15,
|
||||||
outputPrice: 60,
|
outputPrice: 60,
|
||||||
cacheReadsPrice: 7.5,
|
cacheReadsPrice: 7.5,
|
||||||
},
|
systemPromptRole: "developer",
|
||||||
"o1-preview": {
|
supportsTemperature: false,
|
||||||
maxTokens: 32_768,
|
|
||||||
contextWindow: 128_000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 15,
|
|
||||||
outputPrice: 60,
|
|
||||||
cacheReadsPrice: 7.5,
|
|
||||||
},
|
},
|
||||||
"o1-mini": {
|
"o1-mini": {
|
||||||
maxTokens: 65_536,
|
maxTokens: 65_536,
|
||||||
|
|
@ -192,15 +204,8 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 1.1,
|
inputPrice: 1.1,
|
||||||
outputPrice: 4.4,
|
outputPrice: 4.4,
|
||||||
cacheReadsPrice: 0.55,
|
cacheReadsPrice: 0.55,
|
||||||
},
|
systemPromptRole: "developer",
|
||||||
"gpt-4.5-preview": {
|
supportsTemperature: false,
|
||||||
maxTokens: 16_384,
|
|
||||||
contextWindow: 128_000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 75,
|
|
||||||
outputPrice: 150,
|
|
||||||
cacheReadsPrice: 37.5,
|
|
||||||
},
|
},
|
||||||
"gpt-4o": {
|
"gpt-4o": {
|
||||||
maxTokens: 16_384,
|
maxTokens: 16_384,
|
||||||
|
|
@ -210,6 +215,9 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 2.5,
|
inputPrice: 2.5,
|
||||||
outputPrice: 10,
|
outputPrice: 10,
|
||||||
cacheReadsPrice: 1.25,
|
cacheReadsPrice: 1.25,
|
||||||
|
systemPromptRole: "system",
|
||||||
|
defaultTemperature: 0,
|
||||||
|
supportsTemperature: true,
|
||||||
},
|
},
|
||||||
"gpt-4o-mini": {
|
"gpt-4o-mini": {
|
||||||
maxTokens: 16_384,
|
maxTokens: 16_384,
|
||||||
|
|
@ -219,6 +227,8 @@ export const openAiNativeModels = {
|
||||||
inputPrice: 0.15,
|
inputPrice: 0.15,
|
||||||
outputPrice: 0.6,
|
outputPrice: 0.6,
|
||||||
cacheReadsPrice: 0.075,
|
cacheReadsPrice: 0.075,
|
||||||
|
systemPromptRole: "system",
|
||||||
|
defaultTemperature: 0,
|
||||||
},
|
},
|
||||||
} as const satisfies Record<string, ModelInfo>
|
} as const satisfies Record<string, ModelInfo>
|
||||||
|
|
||||||
|
|
@ -229,13 +239,11 @@ export const openAiModelInfoSaneDefaults: ModelInfo = {
|
||||||
supportsPromptCache: false,
|
supportsPromptCache: false,
|
||||||
inputPrice: 0,
|
inputPrice: 0,
|
||||||
outputPrice: 0,
|
outputPrice: 0,
|
||||||
|
defaultTemperature: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
|
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
|
||||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs
|
// https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#api-specs
|
||||||
export const azureOpenAiDefaultApiVersion = "2024-08-01-preview"
|
export const azureOpenAiDefaultApiVersion = "2024-08-01-preview"
|
||||||
|
|
||||||
export const OPENAI_NATIVE_DEFAULT_TEMPERATURE = 0
|
|
||||||
export const GPT5_DEFAULT_TEMPERATURE = 1.0
|
|
||||||
|
|
||||||
export const OPENAI_AZURE_AI_INFERENCE_PATH = "/models/chat/completions"
|
export const OPENAI_AZURE_AI_INFERENCE_PATH = "/models/chat/completions"
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,20 @@ export interface ApiHandlerCreateMessageMetadata {
|
||||||
* Used to enforce "skip once" after a condense operation.
|
* Used to enforce "skip once" after a condense operation.
|
||||||
*/
|
*/
|
||||||
suppressPreviousResponseId?: boolean
|
suppressPreviousResponseId?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force this call to operate statelessly (providers should set store=false and
|
||||||
|
* suppress any previous_response_id). Intended for the first call after local
|
||||||
|
* context rewriting (condense or sliding-window).
|
||||||
|
*/
|
||||||
|
forceStateless?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional stable cache key for OpenAI Responses API caching.
|
||||||
|
* When provided, providers that support it should pass it as prompt_cache_key.
|
||||||
|
* Per-call metadata takes precedence over handler options.
|
||||||
|
*/
|
||||||
|
promptCacheKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiHandler {
|
export interface ApiHandler {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
76
src/core/task-persistence/providerState.ts
Normal file
76
src/core/task-persistence/providerState.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import * as path from "path"
|
||||||
|
import * as fs from "fs/promises"
|
||||||
|
|
||||||
|
import { safeWriteJson } from "../../utils/safeWriteJson"
|
||||||
|
import { fileExistsAtPath } from "../../utils/fs"
|
||||||
|
import { getTaskDirectoryPath } from "../../utils/storage"
|
||||||
|
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistent state for OpenAI Native (Responses API) provider.
|
||||||
|
* Stores encrypted reasoning content (via conversationHistory) and lineage (lastResponseId)
|
||||||
|
* so stateless flows can be resumed across pauses, crashes, and task switches.
|
||||||
|
*/
|
||||||
|
export type OpenAiNativePersistentState = {
|
||||||
|
lastResponseId?: string
|
||||||
|
conversationHistory: any[]
|
||||||
|
/**
|
||||||
|
* Pairing of assistant turn -> its encrypted reasoning artifact, for precise stateless restoration.
|
||||||
|
* Each item is a Responses API input item containing the encrypted artifact for that responseId.
|
||||||
|
*/
|
||||||
|
encryptedArtifacts?: Array<{ responseId: string; item: any }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReadOpenAiNativeStateOptions = {
|
||||||
|
taskId: string
|
||||||
|
globalStoragePath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SaveOpenAiNativeStateOptions = ReadOpenAiNativeStateOptions & {
|
||||||
|
state: OpenAiNativePersistentState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read provider state persisted for a specific task.
|
||||||
|
* Returns undefined if no state exists yet.
|
||||||
|
*/
|
||||||
|
export async function readOpenAiNativeState({
|
||||||
|
taskId,
|
||||||
|
globalStoragePath,
|
||||||
|
}: ReadOpenAiNativeStateOptions): Promise<OpenAiNativePersistentState | undefined> {
|
||||||
|
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
|
||||||
|
const filePath = path.join(taskDir, GlobalFileNames.openAiNativeState)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await fileExistsAtPath(filePath)) {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8")
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
// Basic shape sanity
|
||||||
|
if (parsed && typeof parsed === "object" && Array.isArray(parsed.conversationHistory || [])) {
|
||||||
|
return parsed as OpenAiNativePersistentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[OpenAiNativeState] Failed to read state for task ${taskId}:`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist provider state to the task directory (atomic via safeWriteJson).
|
||||||
|
*/
|
||||||
|
export async function saveOpenAiNativeState({
|
||||||
|
taskId,
|
||||||
|
globalStoragePath,
|
||||||
|
state,
|
||||||
|
}: SaveOpenAiNativeStateOptions): Promise<void> {
|
||||||
|
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
|
||||||
|
const filePath = path.join(taskDir, GlobalFileNames.openAiNativeState)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await safeWriteJson(filePath, state)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[OpenAiNativeState] Failed to write state for task ${taskId}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -254,6 +254,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
isAssistantMessageParserEnabled = false
|
isAssistantMessageParserEnabled = false
|
||||||
private lastUsedInstructions?: string
|
private lastUsedInstructions?: string
|
||||||
private skipPrevResponseIdOnce: boolean = false
|
private skipPrevResponseIdOnce: boolean = false
|
||||||
|
private forceStatelessNextCallOnce: boolean = false
|
||||||
|
// Re-entrancy guard for the first post-condense/sliding-window call
|
||||||
|
private _postCondenseFirstCallScheduled?: boolean
|
||||||
|
private _postCondenseFirstCallInFlight?: boolean
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -815,6 +819,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
{ isNonInteractive: true } /* options */,
|
{ isNonInteractive: true } /* options */,
|
||||||
contextCondense,
|
contextCondense,
|
||||||
)
|
)
|
||||||
|
// Ensure the immediate next model call is stateless after manual condense,
|
||||||
|
// and suppress previous_response_id once to avoid lineage mismatches.
|
||||||
|
this.skipPrevResponseIdOnce = true
|
||||||
|
this.forceStatelessNextCallOnce = true
|
||||||
|
// Mark that the immediate next call is the post-condense first call (one-shot)
|
||||||
|
this._postCondenseFirstCallScheduled = true
|
||||||
|
this._postCondenseFirstCallInFlight = false
|
||||||
|
try {
|
||||||
|
this.providerRef
|
||||||
|
.deref()
|
||||||
|
?.log(`[post-condense] manual condense scheduled first-turn stateless call for task ${this.taskId}`)
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async say(
|
async say(
|
||||||
|
|
@ -1989,6 +2005,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ??
|
state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ??
|
||||||
"default"
|
"default"
|
||||||
|
|
||||||
|
// Track whether the immediate next call must be stateless due to local context rewriting.
|
||||||
|
let forceStatelessNextCall = false
|
||||||
|
|
||||||
const truncateResult = await truncateConversationIfNeeded({
|
const truncateResult = await truncateConversationIfNeeded({
|
||||||
messages: this.apiConversationHistory,
|
messages: this.apiConversationHistory,
|
||||||
totalTokens: contextTokens,
|
totalTokens: contextTokens,
|
||||||
|
|
@ -2004,15 +2023,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
profileThresholds,
|
profileThresholds,
|
||||||
currentProfileId,
|
currentProfileId,
|
||||||
})
|
})
|
||||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
|
||||||
|
const didRewriteContext = truncateResult.messages !== this.apiConversationHistory
|
||||||
|
|
||||||
|
if (didRewriteContext) {
|
||||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (truncateResult.error) {
|
if (truncateResult.error) {
|
||||||
await this.say("condense_context_error", truncateResult.error)
|
await this.say("condense_context_error", truncateResult.error)
|
||||||
} else if (truncateResult.summary) {
|
} else if (truncateResult.summary) {
|
||||||
// A condense operation occurred; for the next GPT‑5 API call we should NOT
|
// A condense operation occurred; for the next GPT‑5 API call we should NOT
|
||||||
// send previous_response_id so the request reflects the fresh condensed context.
|
// send previous_response_id so the request reflects the fresh condensed context.
|
||||||
this.skipPrevResponseIdOnce = true
|
this.skipPrevResponseIdOnce = true
|
||||||
|
forceStatelessNextCall = true
|
||||||
|
|
||||||
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
|
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
|
||||||
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
|
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
|
||||||
|
|
@ -2026,6 +2050,36 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
{ isNonInteractive: true } /* options */,
|
{ isNonInteractive: true } /* options */,
|
||||||
contextCondense,
|
contextCondense,
|
||||||
)
|
)
|
||||||
|
} else if (didRewriteContext) {
|
||||||
|
// Sliding-window truncation occurred (messages changed without a condense summary).
|
||||||
|
// Force the immediate next call to be stateless to align server state with locally rewritten context.
|
||||||
|
forceStatelessNextCall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the decision for this turn so we can include it in metadata for the next call.
|
||||||
|
if (forceStatelessNextCall) {
|
||||||
|
this.forceStatelessNextCallOnce = true
|
||||||
|
// Schedule one-shot guard for the first call after condense/sliding-window.
|
||||||
|
// Do not reset inFlight if a first-turn call is already in progress.
|
||||||
|
if (!this._postCondenseFirstCallScheduled) {
|
||||||
|
this._postCondenseFirstCallScheduled = true
|
||||||
|
this._postCondenseFirstCallInFlight = false
|
||||||
|
try {
|
||||||
|
this.providerRef
|
||||||
|
.deref()
|
||||||
|
?.log(
|
||||||
|
`[post-condense] scheduled first-turn guard (stateless next call) for task ${this.taskId}`,
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.providerRef
|
||||||
|
.deref()
|
||||||
|
?.log(
|
||||||
|
`[post-condense] guard already scheduled; leaving in-flight state unchanged (task ${this.taskId})`,
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2076,12 +2130,36 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
...(previousResponseId ? { previousResponseId } : {}),
|
...(previousResponseId ? { previousResponseId } : {}),
|
||||||
// If a condense just occurred, explicitly suppress continuity fallback for the next call
|
// If a condense just occurred, explicitly suppress continuity fallback for the next call
|
||||||
...(this.skipPrevResponseIdOnce ? { suppressPreviousResponseId: true } : {}),
|
...(this.skipPrevResponseIdOnce ? { suppressPreviousResponseId: true } : {}),
|
||||||
|
// If either condense or sliding-window rewrote the local context, force stateless for the next call.
|
||||||
|
...(this.forceStatelessNextCallOnce ? { forceStateless: true } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset skip flag after applying (it only affects the immediate next call)
|
// Reset one-shot flags after applying (they only affect the immediate next call)
|
||||||
if (this.skipPrevResponseIdOnce) {
|
if (this.skipPrevResponseIdOnce) {
|
||||||
this.skipPrevResponseIdOnce = false
|
this.skipPrevResponseIdOnce = false
|
||||||
}
|
}
|
||||||
|
if (this.forceStatelessNextCallOnce) {
|
||||||
|
this.forceStatelessNextCallOnce = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-entrancy guard: one-shot in-flight guard for the first post-condense/sliding-window call.
|
||||||
|
// If an external second trigger arrives while the first is in-flight, no-op the duplicate.
|
||||||
|
if (this._postCondenseFirstCallScheduled) {
|
||||||
|
if (this._postCondenseFirstCallInFlight && retryAttempt === 0) {
|
||||||
|
// Duplicate external trigger detected - no-op for this call
|
||||||
|
try {
|
||||||
|
this.providerRef
|
||||||
|
.deref()
|
||||||
|
?.log(`[post-condense] suppressing duplicate first-turn trigger (task ${this.taskId})`)
|
||||||
|
} catch {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Acquire the guard for this first-call window
|
||||||
|
this._postCondenseFirstCallInFlight = true
|
||||||
|
try {
|
||||||
|
this.providerRef.deref()?.log(`[post-condense] acquired first-turn guard (task ${this.taskId})`)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
const stream = this.api.createMessage(systemPrompt, cleanConversationHistory, metadata)
|
const stream = this.api.createMessage(systemPrompt, cleanConversationHistory, metadata)
|
||||||
const iterator = stream[Symbol.asyncIterator]()
|
const iterator = stream[Symbol.asyncIterator]()
|
||||||
|
|
@ -2150,6 +2228,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
// incremented retry count.
|
// incremented retry count.
|
||||||
yield* this.attemptApiRequest(retryAttempt + 1)
|
yield* this.attemptApiRequest(retryAttempt + 1)
|
||||||
|
|
||||||
|
// After the retried call completes, release the post-condense guard
|
||||||
|
this._postCondenseFirstCallScheduled = false
|
||||||
|
this._postCondenseFirstCallInFlight = false
|
||||||
|
try {
|
||||||
|
this.providerRef
|
||||||
|
.deref()
|
||||||
|
?.log(`[post-condense] released first-turn guard after retry completion (task ${this.taskId})`)
|
||||||
|
} catch {}
|
||||||
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
const { response } = await this.ask(
|
const { response } = await this.ask(
|
||||||
|
|
@ -2167,6 +2254,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
|
|
||||||
// Delegate generator output from the recursive call.
|
// Delegate generator output from the recursive call.
|
||||||
yield* this.attemptApiRequest()
|
yield* this.attemptApiRequest()
|
||||||
|
|
||||||
|
// After the retried call completes, release the post-condense guard
|
||||||
|
this._postCondenseFirstCallScheduled = false
|
||||||
|
this._postCondenseFirstCallInFlight = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2180,6 +2271,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
// effectively passes along all subsequent chunks from the original
|
// effectively passes along all subsequent chunks from the original
|
||||||
// stream.
|
// stream.
|
||||||
yield* iterator
|
yield* iterator
|
||||||
|
// Release one-shot post-condense guard after successful stream completion
|
||||||
|
this._postCondenseFirstCallScheduled = false
|
||||||
|
this._postCondenseFirstCallInFlight = false
|
||||||
|
try {
|
||||||
|
this.providerRef
|
||||||
|
.deref()
|
||||||
|
?.log(`[post-condense] released first-turn guard after completion (task ${this.taskId})`)
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkpoints
|
// Checkpoints
|
||||||
|
|
@ -2257,6 +2356,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
|
||||||
|
public get isPostCondenseFirstCallScheduled(): boolean {
|
||||||
|
return !!this._postCondenseFirstCallScheduled
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isPostCondenseFirstCallInFlight(): boolean {
|
||||||
|
return !!this._postCondenseFirstCallInFlight
|
||||||
|
}
|
||||||
|
|
||||||
public get cwd() {
|
public get cwd() {
|
||||||
return this.workspacePath
|
return this.workspacePath
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-sear
|
||||||
import { MultiFileSearchReplaceDiffStrategy } from "../../diff/strategies/multi-file-search-replace"
|
import { MultiFileSearchReplaceDiffStrategy } from "../../diff/strategies/multi-file-search-replace"
|
||||||
import { EXPERIMENT_IDS } from "../../../shared/experiments"
|
import { EXPERIMENT_IDS } from "../../../shared/experiments"
|
||||||
|
|
||||||
|
import * as slidingWindowModule from "../../sliding-window"
|
||||||
// Mock delay before any imports that might use it
|
// Mock delay before any imports that might use it
|
||||||
vi.mock("delay", () => ({
|
vi.mock("delay", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
|
|
@ -1495,3 +1496,152 @@ describe("Cline", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Additional tests for stateless override behavior after condense/sliding-window
|
||||||
|
|
||||||
|
describe("Stateless overrides after context rewriting", () => {
|
||||||
|
const makeSimpleStream = (text: string = "ok"): AsyncGenerator<ApiStreamChunk> =>
|
||||||
|
(async function* () {
|
||||||
|
yield { type: "text", text } as any
|
||||||
|
})() as any
|
||||||
|
|
||||||
|
const makeProvider = () =>
|
||||||
|
({
|
||||||
|
context: { globalStorageUri: { fsPath: "/tmp/test-storage" } },
|
||||||
|
getState: vi.fn().mockResolvedValue({
|
||||||
|
// minimal state used by attemptApiRequest
|
||||||
|
apiConfiguration: { apiProvider: "anthropic", apiModelId: "claude-3" },
|
||||||
|
autoApprovalEnabled: true,
|
||||||
|
alwaysApproveResubmit: false,
|
||||||
|
requestDelaySeconds: 0,
|
||||||
|
autoCondenseContext: true,
|
||||||
|
autoCondenseContextPercent: 100,
|
||||||
|
profileThresholds: {},
|
||||||
|
listApiConfigMeta: [],
|
||||||
|
}),
|
||||||
|
postStateToWebview: vi.fn().mockResolvedValue(undefined),
|
||||||
|
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
|
||||||
|
updateTaskHistory: vi.fn().mockResolvedValue(undefined),
|
||||||
|
log: vi.fn(),
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
it("passes metadata.forceStateless=true (and suppressPreviousResponseId) on the next call after condense", async () => {
|
||||||
|
const provider = makeProvider()
|
||||||
|
const cline = new Task({
|
||||||
|
provider,
|
||||||
|
apiConfiguration: { apiProvider: "anthropic", apiModelId: "claude-3" },
|
||||||
|
task: "test",
|
||||||
|
startTask: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Force contextTokens > 0 so the condense/sliding-window logic runs
|
||||||
|
vi.spyOn(cline, "getTokenUsage").mockReturnValue({ contextTokens: 100 } as any)
|
||||||
|
|
||||||
|
// Mock truncateConversationIfNeeded to simulate a condense (summary present)
|
||||||
|
const condenseMessages = [
|
||||||
|
{ role: "user", content: [{ type: "text", text: "Please continue from the following summary:" }] },
|
||||||
|
{ role: "assistant", content: [{ type: "text", text: "Condensed summary" }], isSummary: true },
|
||||||
|
] as any
|
||||||
|
const truncateSpy = vi.spyOn(slidingWindowModule, "truncateConversationIfNeeded").mockResolvedValue({
|
||||||
|
messages: condenseMessages,
|
||||||
|
summary: "Condensed summary",
|
||||||
|
cost: 0,
|
||||||
|
newContextTokens: 50,
|
||||||
|
prevContextTokens: 100,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Spy on createMessage to capture metadata
|
||||||
|
const cmSpy = vi.spyOn(cline.api, "createMessage").mockReturnValue(makeSimpleStream("done"))
|
||||||
|
|
||||||
|
const it1 = cline.attemptApiRequest(0)
|
||||||
|
await it1.next()
|
||||||
|
|
||||||
|
expect(truncateSpy).toHaveBeenCalled()
|
||||||
|
expect(cmSpy).toHaveBeenCalled()
|
||||||
|
const call = cmSpy.mock.calls[0]
|
||||||
|
const metadata = call?.[2] as any
|
||||||
|
expect(metadata).toBeDefined()
|
||||||
|
expect(metadata.forceStateless).toBe(true)
|
||||||
|
// After condense we also suppress previous_response_id
|
||||||
|
expect(metadata.suppressPreviousResponseId).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes metadata.forceStateless=true (without suppressPreviousResponseId) on the next call after sliding-window truncation", async () => {
|
||||||
|
const provider = makeProvider()
|
||||||
|
const cline = new Task({
|
||||||
|
provider,
|
||||||
|
apiConfiguration: { apiProvider: "anthropic", apiModelId: "claude-3" },
|
||||||
|
task: "test",
|
||||||
|
startTask: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Force contextTokens > 0 so the condense/sliding-window logic runs
|
||||||
|
vi.spyOn(cline, "getTokenUsage").mockReturnValue({ contextTokens: 200 } as any)
|
||||||
|
|
||||||
|
// Mock truncateConversationIfNeeded to simulate sliding-window truncation (no summary, messages changed)
|
||||||
|
const truncatedMessages = [
|
||||||
|
{ role: "user", content: [{ type: "text", text: "First message" }] },
|
||||||
|
{ role: "assistant", content: [{ type: "text", text: "Fourth message" }] },
|
||||||
|
{ role: "user", content: [{ type: "text", text: "Fifth message" }] },
|
||||||
|
] as any
|
||||||
|
const truncateSpy = vi.spyOn(slidingWindowModule, "truncateConversationIfNeeded").mockResolvedValue({
|
||||||
|
messages: truncatedMessages,
|
||||||
|
summary: "",
|
||||||
|
cost: 0,
|
||||||
|
prevContextTokens: 200,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Spy on createMessage to capture metadata
|
||||||
|
const cmSpy = vi.spyOn(cline.api, "createMessage").mockReturnValue(makeSimpleStream("done"))
|
||||||
|
|
||||||
|
const it1 = cline.attemptApiRequest(0)
|
||||||
|
await it1.next()
|
||||||
|
|
||||||
|
expect(truncateSpy).toHaveBeenCalled()
|
||||||
|
expect(cmSpy).toHaveBeenCalled()
|
||||||
|
const call = cmSpy.mock.calls[0]
|
||||||
|
const metadata = call?.[2] as any
|
||||||
|
expect(metadata).toBeDefined()
|
||||||
|
expect(metadata.forceStateless).toBe(true)
|
||||||
|
// Sliding-window path does not set suppressPreviousResponseId in metadata (provider will suppress via forceStateless)
|
||||||
|
expect(metadata.suppressPreviousResponseId).toBeUndefined()
|
||||||
|
})
|
||||||
|
it("only initiates one provider call for the first post-condense turn, even if two triggers fire", async () => {
|
||||||
|
const provider = makeProvider()
|
||||||
|
const cline = new Task({
|
||||||
|
provider,
|
||||||
|
apiConfiguration: { apiProvider: "anthropic", apiModelId: "claude-3" },
|
||||||
|
task: "test",
|
||||||
|
startTask: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure condense/sliding-window logic runs
|
||||||
|
vi.spyOn(cline, "getTokenUsage").mockReturnValue({ contextTokens: 100 } as any)
|
||||||
|
|
||||||
|
// Mock condense result to schedule the first post-condense call as stateless
|
||||||
|
const condenseMessages = [
|
||||||
|
{ role: "user", content: [{ type: "text", text: "Please continue from summary" }] },
|
||||||
|
{ role: "assistant", content: [{ type: "text", text: "Condensed summary" }], isSummary: true },
|
||||||
|
] as any
|
||||||
|
vi.spyOn(slidingWindowModule, "truncateConversationIfNeeded").mockResolvedValue({
|
||||||
|
messages: condenseMessages,
|
||||||
|
summary: "Condensed summary",
|
||||||
|
cost: 0,
|
||||||
|
newContextTokens: 50,
|
||||||
|
prevContextTokens: 100,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Spy on provider call and return a simple stream
|
||||||
|
const cmSpy = vi.spyOn(cline.api, "createMessage").mockReturnValue(makeSimpleStream("done"))
|
||||||
|
|
||||||
|
// Fire two triggers for the "first turn after condense"
|
||||||
|
const it1 = cline.attemptApiRequest(0)
|
||||||
|
await it1.next() // enters request, sets in-flight guard
|
||||||
|
|
||||||
|
const it2 = cline.attemptApiRequest(0)
|
||||||
|
await it2.next() // should no-op due to re-entrancy guard
|
||||||
|
|
||||||
|
// Exactly one provider invocation
|
||||||
|
expect(cmSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,18 @@ export const webviewMessageHandler = async (
|
||||||
|
|
||||||
// Initialize with history item after deletion
|
// Initialize with history item after deletion
|
||||||
await provider.initClineWithHistoryItem(historyItem)
|
await provider.initClineWithHistoryItem(historyItem)
|
||||||
|
|
||||||
|
// Invalidate GPT‑5 continuity for the newly initialized task so the next call does NOT
|
||||||
|
// send previous_response_id (prevents mismatched lineage after delete/trim).
|
||||||
|
try {
|
||||||
|
const newCline = provider.getCurrentCline()
|
||||||
|
if (newCline) {
|
||||||
|
// Call overwriteClineMessages with the same array to trigger the one-turn suppression flag.
|
||||||
|
await newCline.overwriteClineMessages(newCline.clineMessages)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to invalidate continuity after delete:", e)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in delete message:", error)
|
console.error("Error in delete message:", error)
|
||||||
vscode.window.showErrorMessage(
|
vscode.window.showErrorMessage(
|
||||||
|
|
@ -345,9 +357,27 @@ export const webviewMessageHandler = async (
|
||||||
await updateGlobalState("alwaysAllowUpdateTodoList", message.bool)
|
await updateGlobalState("alwaysAllowUpdateTodoList", message.bool)
|
||||||
await provider.postStateToWebview()
|
await provider.postStateToWebview()
|
||||||
break
|
break
|
||||||
case "askResponse":
|
case "askResponse": {
|
||||||
|
const cline = provider.getCurrentCline?.()
|
||||||
|
// Optional single-flight guard: if the special first post-condense turn is in-flight,
|
||||||
|
// suppress duplicate UI-triggered sends to avoid racing a scheduled stateless call.
|
||||||
|
if (
|
||||||
|
cline &&
|
||||||
|
typeof (cline as any).isPostCondenseFirstCallScheduled === "boolean" &&
|
||||||
|
typeof (cline as any).isPostCondenseFirstCallInFlight === "boolean" &&
|
||||||
|
(cline as any).isPostCondenseFirstCallScheduled &&
|
||||||
|
(cline as any).isPostCondenseFirstCallInFlight
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
provider.log?.(
|
||||||
|
`[webview] askResponse suppressed during post-condense first-turn in-flight for task ${(cline as any).taskId}`,
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
break
|
||||||
|
}
|
||||||
provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case "autoCondenseContext":
|
case "autoCondenseContext":
|
||||||
await updateGlobalState("autoCondenseContext", message.bool)
|
await updateGlobalState("autoCondenseContext", message.bool)
|
||||||
await provider.postStateToWebview()
|
await provider.postStateToWebview()
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,20 @@ export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider"> & {
|
||||||
* Defaults to true; set to false to disable summaries.
|
* Defaults to true; set to false to disable summaries.
|
||||||
*/
|
*/
|
||||||
enableGpt5ReasoningSummary?: boolean
|
enableGpt5ReasoningSummary?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls statefulness for Responses API.
|
||||||
|
* When false, treat interactions as stateless and avoid using previous_response_id.
|
||||||
|
* The provider will include encrypted reasoning content to allow passing it back explicitly.
|
||||||
|
* Defaults to true (stateful) if not provided.
|
||||||
|
*/
|
||||||
|
store?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional default cache key for OpenAI Responses API prompt bucketing.
|
||||||
|
* Per-call metadata.promptCacheKey takes precedence when provided.
|
||||||
|
*/
|
||||||
|
promptCacheKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouterName
|
// RouterName
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@ export const GlobalFileNames = {
|
||||||
mcpSettings: "mcp_settings.json",
|
mcpSettings: "mcp_settings.json",
|
||||||
customModes: "custom_modes.yaml",
|
customModes: "custom_modes.yaml",
|
||||||
taskMetadata: "task_metadata.json",
|
taskMetadata: "task_metadata.json",
|
||||||
|
openAiNativeState: "openai_native_state.json",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,8 @@ export const ChatRowContent = ({
|
||||||
}: ChatRowContentProps) => {
|
}: ChatRowContentProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
|
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
|
||||||
const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
|
const [reasoningCollapsed, setReasoningCollapsed] = useState<boolean>(true)
|
||||||
|
|
||||||
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
|
const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
|
||||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ export const ReasoningBlock = ({ content, elapsed, isCollapsed = false, onToggle
|
||||||
processNextTransition()
|
processNextTransition()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update the preview line only when there's a meaningful delta
|
||||||
|
// Restore previous thresholded behavior to keep collapsed header UX (counter) stable.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (content.length - cursorRef.current > 160) {
|
if (content.length - cursorRef.current > 160) {
|
||||||
setThought("... " + content.slice(cursorRef.current))
|
setThought("... " + content.slice(cursorRef.current))
|
||||||
|
|
|
||||||
|
|
@ -648,11 +648,14 @@ const ApiOptions = ({
|
||||||
fuzzyMatchThreshold={apiConfiguration.fuzzyMatchThreshold}
|
fuzzyMatchThreshold={apiConfiguration.fuzzyMatchThreshold}
|
||||||
onChange={(field, value) => setApiConfigurationField(field, value)}
|
onChange={(field, value) => setApiConfigurationField(field, value)}
|
||||||
/>
|
/>
|
||||||
<TemperatureControl
|
{/* Hide temperature UI when the selected model does not support temperature */}
|
||||||
value={apiConfiguration.modelTemperature}
|
{selectedModelInfo?.supportsTemperature !== false && (
|
||||||
onChange={handleInputChange("modelTemperature", noTransform)}
|
<TemperatureControl
|
||||||
maxValue={2}
|
value={apiConfiguration.modelTemperature}
|
||||||
/>
|
onChange={handleInputChange("modelTemperature", noTransform)}
|
||||||
|
maxValue={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<RateLimitSecondsControl
|
<RateLimitSecondsControl
|
||||||
value={apiConfiguration.rateLimitSeconds || 0}
|
value={apiConfiguration.rateLimitSeconds || 0}
|
||||||
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
|
onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)}
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,16 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod
|
||||||
// Only show minimal for OpenAI Native provider GPT-5 models
|
// Only show minimal for OpenAI Native provider GPT-5 models
|
||||||
const isOpenAiNativeProvider = apiConfiguration.apiProvider === "openai-native"
|
const isOpenAiNativeProvider = apiConfiguration.apiProvider === "openai-native"
|
||||||
const isGpt5Model = isOpenAiNativeProvider && selectedModelId && selectedModelId.startsWith("gpt-5")
|
const isGpt5Model = isOpenAiNativeProvider && selectedModelId && selectedModelId.startsWith("gpt-5")
|
||||||
// Add "minimal" option for GPT-5 models
|
// Build list of efforts:
|
||||||
// Spread to convert readonly tuple into a mutable array, then expose as readonly for safety
|
// - If GPT‑5 (OpenAI Native), include "minimal"
|
||||||
|
// - Otherwise hide "minimal"
|
||||||
|
// Also de‑dupe in case the source list already includes "minimal"
|
||||||
const baseEfforts = [...reasoningEfforts] as ReasoningEffortWithMinimal[]
|
const baseEfforts = [...reasoningEfforts] as ReasoningEffortWithMinimal[]
|
||||||
|
const withMinimal = Array.from(new Set<ReasoningEffortWithMinimal>(["minimal", ...baseEfforts]))
|
||||||
|
const withoutMinimal = baseEfforts.filter((v) => v !== "minimal") as ReasoningEffortWithMinimal[]
|
||||||
const availableReasoningEfforts: ReadonlyArray<ReasoningEffortWithMinimal> = isGpt5Model
|
const availableReasoningEfforts: ReadonlyArray<ReasoningEffortWithMinimal> = isGpt5Model
|
||||||
? (["minimal", ...baseEfforts] as ReasoningEffortWithMinimal[])
|
? withMinimal
|
||||||
: baseEfforts
|
: withoutMinimal
|
||||||
|
|
||||||
// Default reasoning effort - use model's default if available
|
// Default reasoning effort - use model's default if available
|
||||||
// GPT-5 models have "medium" as their default in the model configuration
|
// GPT-5 models have "medium" as their default in the model configuration
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue