feat(models): add GPT-5.4 fast mode support

This commit is contained in:
James Peter
2026-03-06 10:51:21 +10:00
parent fe65fa5543
commit 5082189656
57 changed files with 1697 additions and 148 deletions
@@ -662,6 +662,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -808,6 +818,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"InitializeCapabilities": {
"description": "Client-declared capabilities negotiated during initialize.",
"properties": {
@@ -1722,7 +1741,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -1779,6 +1798,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -3069,6 +3069,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -3146,6 +3156,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -4171,7 +4190,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -4228,6 +4247,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -3780,6 +3780,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -3880,6 +3890,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"ItemCompletedNotification": {
"properties": {
"item": {
@@ -5314,7 +5333,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -5371,6 +5390,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -5514,6 +5514,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -5820,6 +5830,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"InitializeCapabilities": {
"description": "Client-declared capabilities negotiated during initialize.",
"properties": {
@@ -7471,7 +7490,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -7528,6 +7547,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -12286,6 +12339,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/v2/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -12428,6 +12491,15 @@
},
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"InputModality": {
"description": "Canonical user-input modality tags advertised by a model.",
"oneOf": [
@@ -12951,6 +13023,16 @@
},
"Model": {
"properties": {
"availabilityNux": {
"anyOf": [
{
"$ref": "#/definitions/v2/ModelAvailabilityNux"
},
{
"type": "null"
}
]
},
"defaultReasoningEffort": {
"$ref": "#/definitions/v2/ReasoningEffort"
},
@@ -12997,6 +13079,16 @@
"string",
"null"
]
},
"upgradeInfo": {
"anyOf": [
{
"$ref": "#/definitions/v2/ModelUpgradeInfo"
},
{
"type": "null"
}
]
}
},
"required": [
@@ -13011,6 +13103,17 @@
],
"type": "object"
},
"ModelAvailabilityNux": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
},
"ModelListParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -13064,6 +13167,35 @@
"title": "ModelListResponse",
"type": "object"
},
"ModelUpgradeInfo": {
"properties": {
"migrationMarkdown": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"modelLink": {
"type": [
"string",
"null"
]
},
"upgradeCopy": {
"type": [
"string",
"null"
]
}
},
"required": [
"model"
],
"type": "object"
},
"NetworkAccess": {
"enum": [
"restricted",
@@ -14011,7 +14143,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/v2/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -14068,6 +14200,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -3069,6 +3069,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -3146,6 +3156,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -4171,7 +4190,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -4228,6 +4247,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -150,6 +150,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -206,6 +216,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -700,7 +719,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -757,6 +776,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -3069,6 +3069,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -3146,6 +3156,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -4171,7 +4190,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -4228,6 +4247,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -3069,6 +3069,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -3146,6 +3156,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -4171,7 +4190,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -4228,6 +4247,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -22,6 +22,16 @@
},
"Model": {
"properties": {
"availabilityNux": {
"anyOf": [
{
"$ref": "#/definitions/ModelAvailabilityNux"
},
{
"type": "null"
}
]
},
"defaultReasoningEffort": {
"$ref": "#/definitions/ReasoningEffort"
},
@@ -68,6 +78,16 @@
"string",
"null"
]
},
"upgradeInfo": {
"anyOf": [
{
"$ref": "#/definitions/ModelUpgradeInfo"
},
{
"type": "null"
}
]
}
},
"required": [
@@ -82,6 +102,46 @@
],
"type": "object"
},
"ModelAvailabilityNux": {
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"type": "object"
},
"ModelUpgradeInfo": {
"properties": {
"migrationMarkdown": {
"type": [
"string",
"null"
]
},
"model": {
"type": "string"
},
"modelLink": {
"type": [
"string",
"null"
]
},
"upgradeCopy": {
"type": [
"string",
"null"
]
}
},
"required": [
"model"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -103,6 +103,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -159,6 +169,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -551,7 +570,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -608,6 +627,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -145,6 +145,16 @@
},
{
"properties": {
"detail": {
"anyOf": [
{
"$ref": "#/definitions/ImageDetail"
},
{
"type": "null"
}
]
},
"image_url": {
"type": "string"
},
@@ -201,6 +211,15 @@
],
"type": "object"
},
"ImageDetail": {
"enum": [
"auto",
"low",
"high",
"original"
],
"type": "string"
},
"LocalShellAction": {
"oneOf": [
{
@@ -601,7 +620,7 @@
"type": "string"
},
"output": {
"type": "string"
"$ref": "#/definitions/FunctionCallOutputPayload"
},
"type": {
"enum": [
@@ -658,6 +677,40 @@
"title": "WebSearchCallResponseItem",
"type": "object"
},
{
"properties": {
"id": {
"type": "string"
},
"result": {
"type": "string"
},
"revised_prompt": {
"type": [
"string",
"null"
]
},
"status": {
"type": "string"
},
"type": {
"enum": [
"image_generation_call"
],
"title": "ImageGenerationCallResponseItemType",
"type": "string"
}
},
"required": [
"id",
"result",
"status",
"type"
],
"title": "ImageGenerationCallResponseItem",
"type": "object"
},
{
"properties": {
"ghost_commit": {
@@ -1,6 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Thread } from "./Thread";
export type ThreadMetadataUpdateResponse = { thread: Thread, };
export type AddConversationSubscriptionResponse = { subscriptionId: string, };
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RealtimeHandoffMessage = { role: string, text: string, };
export type ArchiveConversationResponse = Record<string, never>;
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ImageGenerationBeginEvent = { call_id: string, };
export type CancelLoginChatGptParams = { loginId: string, };
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ImageDetail = "auto" | "low" | "high" | "original";
export type CancelLoginChatGptResponse = Record<string, never>;
@@ -1,9 +1,10 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImageDetail } from "./ImageDetail";
/**
* Responses API compatible content items that can be returned by a tool call.
* This is a subset of ContentItem with the types we support as function call outputs.
*/
export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, };
export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, detail?: ImageDetail, };
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkPermissions = { enabled: boolean | null, };
export type GetUserAgentResponse = { userAgent: string, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ImageDetail = "auto" | "low" | "high" | "original";
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ServiceTier = "fast" | "flex";
export type LoginApiKeyParams = { apiKey: string, };
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PluginInstallResponse = Record<string, never>;
export type LogoutChatGptResponse = Record<string, never>;
@@ -15,4 +15,4 @@ export type ResponseItem = { "type": "message", role: string, content: Array<Con
/**
* Set when using the Responses API.
*/
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, arguments: string, call_id: string, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputPayload, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, output: string, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, } | { "type": "compaction_summary", encrypted_content: string, } | { "type": "other" };
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, arguments: string, call_id: string, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputPayload, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, output: FunctionCallOutputPayload, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "ghost_snapshot", ghost_commit: GhostCommit, } | { "type": "compaction_summary", encrypted_content: string, } | { "type": "other" };
@@ -89,6 +89,7 @@ export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams";
export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse";
export type { GitSha } from "./GitSha";
export type { HistoryEntry } from "./HistoryEntry";
export type { ImageDetail } from "./ImageDetail";
export type { InitializeCapabilities } from "./InitializeCapabilities";
export type { InitializeParams } from "./InitializeParams";
export type { InitializeResponse } from "./InitializeResponse";
@@ -3,6 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InputModality } from "../InputModality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
import type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
export type Model = { id: string, model: string, upgrade: string | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, isDefault: boolean, };
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, isDefault: boolean, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModelAvailabilityNux = { message: string, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModelUpgradeInfo = { model: string, upgradeCopy: string | null, modelLink: string | null, migrationMarkdown: string | null, };
@@ -98,8 +98,10 @@ export type { McpToolCallResult } from "./McpToolCallResult";
export type { McpToolCallStatus } from "./McpToolCallStatus";
export type { MergeStrategy } from "./MergeStrategy";
export type { Model } from "./Model";
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
export type { ModelListParams } from "./ModelListParams";
export type { ModelListResponse } from "./ModelListResponse";
export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
@@ -492,7 +492,7 @@ pub(crate) fn estimate_item_tokens(item: &ResponseItem) -> usize {
.iter()
.map(|item| match item {
FunctionCallOutputContentItem::InputText { text } => text.len(),
FunctionCallOutputContentItem::InputImage { image_url } => image_url.len() / 10,
FunctionCallOutputContentItem::InputImage { image_url, .. } => image_url.len() / 10,
})
.sum(),
},
@@ -364,7 +364,9 @@ fn estimate_item_tokens(item: &ResponseItem) -> usize {
.iter()
.map(|item| match item {
FunctionCallOutputContentItem::InputText { text } => text.len(),
FunctionCallOutputContentItem::InputImage { image_url } => image_url.len() / 10,
FunctionCallOutputContentItem::InputImage { image_url, .. } => {
image_url.len() / 10
}
})
.sum(),
},
+36
View File
@@ -49,6 +49,36 @@ const ALL_TEXT_VERBOSITY: &[TextVerbosityConfig] = &[
static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
vec![
ModelPreset {
id: "gpt-5.4".to_string(),
model: "gpt-5.4".to_string(),
display_name: "gpt-5.4".to_string(),
description: "Frontier flagship model.".to_string(),
default_reasoning_effort: ReasoningEffort::Medium,
supported_reasoning_efforts: vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low,
description: "Fast responses with lighter reasoning".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Medium,
description: "Balances speed and reasoning depth for everyday tasks".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::High,
description: "Maximizes reasoning depth for complex problems".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::XHigh,
description: "Extra high reasoning depth for complex problems".to_string(),
},
],
supported_text_verbosity: ALL_TEXT_VERBOSITY,
is_default: false,
upgrade: None,
pro_only: false,
show_in_picker: true,
},
ModelPreset {
id: "gpt-5.3-codex".to_string(),
model: "gpt-5.3-codex".to_string(),
@@ -606,6 +636,12 @@ mod tests {
);
}
#[test]
fn gpt_5_4_available_for_api_key_auth() {
let presets = builtin_model_presets(Some(AuthMode::ApiKey), false);
assert!(presets.iter().any(|preset| preset.id == "gpt-5.4"));
}
#[test]
fn clamp_reasoning_effort_downgrades_to_supported_level() {
let clamped = clamp_reasoning_effort_for_model(
+4
View File
@@ -25,6 +25,7 @@ use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::client_common::ResponseStream;
use crate::client_common::replace_image_payloads_for_model;
use crate::client_common::rewrite_image_generation_calls_for_input;
use crate::debug_logger::DebugLogger;
use crate::error::CodexErr;
use crate::error::Result;
@@ -63,6 +64,7 @@ pub(crate) async fn stream_chat_completions(
messages.push(json!({"role": "system", "content": full_instructions}));
let mut input = prompt.get_formatted_input();
rewrite_image_generation_calls_for_input(&mut input);
replace_image_payloads_for_model(&mut input, model_slug);
// Pre-scan: map Reasoning blocks to the adjacent assistant anchor after the last user.
@@ -87,6 +89,7 @@ pub(crate) async fn stream_chat_completions(
ResponseItem::CustomToolCall { .. } => {}
ResponseItem::CustomToolCallOutput { .. } => {}
ResponseItem::WebSearchCall { .. } => {}
ResponseItem::ImageGenerationCall { .. } => {}
ResponseItem::GhostSnapshot { .. } => {}
}
}
@@ -283,6 +286,7 @@ pub(crate) async fn stream_chat_completions(
}
ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::GhostSnapshot { .. }
| ResponseItem::Other => {
// Omit these items from the conversation history.
+13
View File
@@ -51,9 +51,11 @@ use crate::client_common::ResponseStream;
use crate::client_common::ResponsesApiRequest;
use crate::client_common::create_reasoning_param_for_request;
use crate::client_common::replace_image_payloads_for_model;
use crate::client_common::rewrite_image_generation_calls_for_input;
use crate::config::Config;
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::config_types::ServiceTier;
use crate::config_types::TextVerbosity as TextVerbosityConfig;
use crate::debug_logger::DebugLogger;
use crate::default_client::create_client;
@@ -680,6 +682,7 @@ impl ModelClient {
}
let mut input_with_instructions = prompt.get_formatted_input();
rewrite_image_generation_calls_for_input(&mut input_with_instructions);
replace_image_payloads_for_model(&mut input_with_instructions, request_model);
let want_format = prompt.text_format.clone().or_else(|| {
@@ -740,6 +743,10 @@ impl ModelClient {
store: self.provider.is_azure_responses_endpoint(),
stream: true,
include,
service_tier: match self.config.service_tier {
Some(ServiceTier::Fast) => Some("priority".to_string()),
_ => None,
},
prompt_cache_key: Some(session_id_str.clone()),
};
@@ -1086,6 +1093,7 @@ impl ModelClient {
}
let mut input_with_instructions = prompt.get_formatted_input();
rewrite_image_generation_calls_for_input(&mut input_with_instructions);
replace_image_payloads_for_model(&mut input_with_instructions, request_model);
// Build `text` parameter with conditional verbosity and optional format.
@@ -1173,6 +1181,10 @@ impl ModelClient {
store: azure_workaround,
stream: true,
include,
service_tier: match self.config.service_tier {
Some(ServiceTier::Fast) => Some("priority".to_string()),
_ => None,
},
// Use a stable per-process cache key (session id). With store=false this is inert.
prompt_cache_key: Some(session_id_str.clone()),
};
@@ -2237,6 +2249,7 @@ fn attach_item_ids(payload_json: &mut Value, original_items: &[ResponseItem]) {
if let ResponseItem::Reasoning { id, .. }
| ResponseItem::Message { id: Some(id), .. }
| ResponseItem::WebSearchCall { id: Some(id), .. }
| ResponseItem::ImageGenerationCall { id, .. }
| ResponseItem::FunctionCall { id: Some(id), .. }
| ResponseItem::LocalShellCall { id: Some(id), .. }
| ResponseItem::CustomToolCall { id: Some(id), .. } = item
+57
View File
@@ -462,6 +462,33 @@ pub(crate) fn replace_image_payloads_for_model(input: &mut Vec<ResponseItem>, mo
}
}
/// Convert upstream `image_generation_call` output items into standard user
/// `input_image` messages when we replay stateless history.
pub(crate) fn rewrite_image_generation_calls_for_input(input: &mut Vec<ResponseItem>) {
let original_items = std::mem::take(input);
*input = original_items
.into_iter()
.map(|item| match item {
ResponseItem::ImageGenerationCall { result, .. } => {
let image_url = if result.starts_with("data:") {
result
} else {
format!("data:image/png;base64,{result}")
};
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputImage { image_url }],
end_turn: None,
phase: None,
}
}
_ => item,
})
.collect();
}
/// Request object that is serialized as JSON and POST'ed when using the
/// Responses API.
#[derive(Debug, Serialize)]
@@ -483,6 +510,8 @@ pub(crate) struct ResponsesApiRequest<'a> {
pub(crate) stream: bool,
pub(crate) include: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) prompt_cache_key: Option<String>,
}
@@ -553,6 +582,7 @@ mod tests {
output: code_protocol::models::FunctionCallOutputPayload::from_content_items(vec![
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,BBB".to_string(),
detail: None,
},
]),
},
@@ -601,6 +631,30 @@ mod tests {
));
}
#[test]
fn rewrite_image_generation_calls_for_input_converts_to_user_image_message() {
let mut input = vec![ResponseItem::ImageGenerationCall {
id: "ig_1".to_string(),
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
}];
rewrite_image_generation_calls_for_input(&mut input);
assert_eq!(input.len(), 1);
assert!(matches!(
&input[0],
ResponseItem::Message { role, content, .. }
if role == "user"
&& matches!(
content.first(),
Some(ContentItem::InputImage { image_url })
if image_url == "data:image/png;base64,Zm9v"
)
));
}
struct InstructionsTestCase {
pub slug: &'static str,
pub expects_apply_patch_instructions: bool,
@@ -722,6 +776,7 @@ mod tests {
store: false,
stream: true,
include: vec![],
service_tier: None,
prompt_cache_key: None,
text: Some(Text { verbosity: OpenAiTextVerbosity::Low, format: None }),
};
@@ -757,6 +812,7 @@ mod tests {
store: false,
stream: true,
include: vec![],
service_tier: None,
prompt_cache_key: None,
text: Some(Text {
verbosity: OpenAiTextVerbosity::Medium,
@@ -804,6 +860,7 @@ mod tests {
store: false,
stream: true,
include: vec![],
service_tier: None,
prompt_cache_key: None,
text: None,
};
+1
View File
@@ -1137,6 +1137,7 @@ impl Codex {
preferred_model_reasoning_effort: config.preferred_model_reasoning_effort,
model_reasoning_summary: config.model_reasoning_summary,
model_text_verbosity: config.model_text_verbosity,
service_tier: config.service_tier,
user_instructions,
base_instructions: config.base_instructions.clone(),
approval_policy: config.approval_policy,
+22 -12
View File
@@ -28,6 +28,7 @@ use crate::agent_tool::external_agent_command_exists;
use crate::protocol::McpListToolsResponseEvent;
use code_app_server_protocol::AuthMode as AppAuthMode;
use code_protocol::models::FunctionCallOutputContentItem;
use code_protocol::models::ImageDetail;
use code_protocol::models::FunctionCallOutputPayload;
use std::collections::HashMap;
@@ -262,6 +263,7 @@ pub(super) async fn submission_loop(
preferred_model_reasoning_effort,
model_reasoning_summary,
model_text_verbosity,
service_tier,
user_instructions: provided_user_instructions,
base_instructions: provided_base_instructions,
approval_policy,
@@ -308,6 +310,7 @@ pub(super) async fn submission_loop(
}
updated_config.model_reasoning_summary = model_reasoning_summary;
updated_config.model_text_verbosity = model_text_verbosity;
updated_config.service_tier = service_tier;
updated_config.user_instructions = provided_user_instructions.clone();
let base_instructions = provided_base_instructions.or_else(|| {
crate::model_family::base_instructions_override_for_personality(
@@ -1959,6 +1962,9 @@ async fn run_agent(sess: Arc<Session>, turn_context: Arc<TurnContext>, sub_id: S
encrypted_content: encrypted_content.clone(),
});
}
(ResponseItem::ImageGenerationCall { .. }, None) => {
items_to_record_in_conversation_history.push(item.clone());
}
_ => {
warn!("Unexpected response item: {item:?} with response: {response:?}");
}
@@ -3645,6 +3651,7 @@ async fn handle_response_item(
}
None
}
ResponseItem::ImageGenerationCall { .. } => None,
ResponseItem::GhostSnapshot { .. } => None,
ResponseItem::Other => None,
};
@@ -5525,16 +5532,12 @@ async fn handle_image_view(sess: &Session, ctx: &ToolCallCtx, arguments: String)
.filter(|text| !text.is_empty())
.unwrap_or(filename);
let marker = format!("[image: {label}]");
let image_message = ResponseInputItem::Message {
role: "user".to_string(),
content: vec![
ContentItem::InputText { text: marker },
ContentItem::InputImage {
image_url: format!("data:{mime};base64,{encoded}"),
},
],
};
sess.add_pending_input(image_message);
let image_url = format!("data:{mime};base64,{encoded}");
let image_detail = sess
.client
.get_model_family()
.supports_image_detail_original
.then_some(ImageDetail::Original);
let order = ctx.order_meta(sess.current_request_ordinal());
let event = sess.make_event_with_order(
@@ -5551,8 +5554,15 @@ async fn handle_image_view(sess: &Session, ctx: &ToolCallCtx, arguments: String)
ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
body: code_protocol::models::FunctionCallOutputBody::Text(format!("attached image: {}", resolved.display())),
success: Some(true)},
body: code_protocol::models::FunctionCallOutputBody::ContentItems(vec![
FunctionCallOutputContentItem::InputText { text: marker },
FunctionCallOutputContentItem::InputImage {
image_url,
detail: image_detail,
},
]),
success: Some(true),
},
}
},
)
+81
View File
@@ -43,6 +43,7 @@ use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::config_types::ReasoningEffort;
use crate::config_types::ReasoningSummary;
use crate::config_types::ServiceTier;
use crate::project_features::{load_project_commands, ProjectCommand, ProjectHooks};
use code_app_server_protocol::AuthMode;
use code_protocol::config_types::SandboxMode;
@@ -413,6 +414,12 @@ pub struct Config {
/// The value to use for `text.verbosity` when making a request using the Responses API.
pub model_text_verbosity: TextVerbosity,
/// Optional service tier preference for model requests.
///
/// `Some(Fast)` sends `service_tier=priority` to the Responses API.
/// `None` sends no override (legacy standard behavior).
pub service_tier: Option<ServiceTier>,
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: String,
@@ -741,6 +748,7 @@ pub struct ConfigToml {
pub model_reasoning_summary: Option<ReasoningSummary>,
pub model_text_verbosity: Option<TextVerbosity>,
pub model_personality: Option<Personality>,
pub service_tier: Option<ServiceTier>,
/// Override to force-enable reasoning summaries for the configured model.
pub model_supports_reasoning_summaries: Option<bool>,
@@ -1250,6 +1258,12 @@ impl Config {
.model_personality
.or(cfg.model_personality);
let service_tier = match config_profile.service_tier.or(cfg.service_tier) {
Some(ServiceTier::Fast) => Some(ServiceTier::Fast),
Some(ServiceTier::Standard) => None,
None => Some(ServiceTier::Fast),
};
let model_family =
find_family_for_model(&model).unwrap_or_else(|| derive_default_model_family(&model));
let default_tool_output_max_bytes = model_family.tool_output_max_bytes();
@@ -1612,6 +1626,7 @@ impl Config {
.model_text_verbosity
.or(cfg.model_text_verbosity)
.unwrap_or_default(),
service_tier,
chatgpt_base_url: config_profile
.chatgpt_base_url
@@ -2939,6 +2954,34 @@ model_verbosity = "high"
assert_eq!(legacy.review_model.as_deref(), Some("gpt-5.1-codex"));
}
#[test]
fn upgrade_legacy_model_slugs_does_not_rewrite_gpt_5_4() {
let mut cfg = ConfigToml {
model: Some("gpt-5.4".to_string()),
review_model: Some("test-gpt-5.4".to_string()),
..Default::default()
};
upgrade_legacy_model_slugs(&mut cfg);
assert_eq!(cfg.model.as_deref(), Some("gpt-5.4"));
assert_eq!(cfg.review_model.as_deref(), Some("test-gpt-5.4"));
}
#[test]
fn upgrade_legacy_model_slugs_repairs_gpt_5_2_4_typo() {
let mut cfg = ConfigToml {
model: Some("gpt-5.2.4".to_string()),
review_model: Some("test-gpt-5.2.4".to_string()),
..Default::default()
};
upgrade_legacy_model_slugs(&mut cfg);
assert_eq!(cfg.model.as_deref(), Some("gpt-5.4"));
assert_eq!(cfg.review_model.as_deref(), Some("test-gpt-5.4"));
}
#[test]
fn test_compact_prompt_override_prefers_cli_string() -> std::io::Result<()> {
let fixture = create_test_fixture()?;
@@ -3121,7 +3164,12 @@ model_verbosity = "high"
#[cfg(test)]
mod agent_merge_tests {
use super::merge_with_default_agents;
use super::Config;
use super::ConfigOverrides;
use super::ConfigToml;
use crate::config_types::AgentConfig;
use crate::config_types::ServiceTier;
use tempfile::TempDir;
fn agent(name: &str, command: &str, enabled: bool) -> AgentConfig {
AgentConfig {
@@ -3275,6 +3323,39 @@ mod agent_merge_tests {
"should dedupe gemini alias/canonical"
);
}
#[test]
fn service_tier_defaults_to_fast_when_unspecified() -> anyhow::Result<()> {
let code_home = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
cwd: Some(code_home.path().to_path_buf()),
..Default::default()
},
code_home.path().to_path_buf(),
)?;
assert_eq!(config.service_tier, Some(ServiceTier::Fast));
Ok(())
}
#[test]
fn service_tier_standard_disables_override() -> anyhow::Result<()> {
let code_home = TempDir::new()?;
let cfg = toml::from_str::<ConfigToml>(r#"service_tier = "standard""#)?;
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides {
cwd: Some(code_home.path().to_path_buf()),
..Default::default()
},
code_home.path().to_path_buf(),
)?;
assert_eq!(config.service_tier, None);
Ok(())
}
}
#[cfg(test)]
+26 -5
View File
@@ -148,11 +148,32 @@ pub(crate) fn upgrade_legacy_model_slugs(cfg: &mut ConfigToml) {
}
fn upgrade_legacy_model_slug(slug: &str) -> Option<String> {
if slug.starts_with("gpt-5.2")
|| slug.starts_with("test-gpt-5.2")
|| slug.starts_with("gpt-5.3")
|| slug.starts_with("test-gpt-5.3")
{
match slug {
"gpt-5.2.4" => return Some("gpt-5.4".to_string()),
"test-gpt-5.2.4" => return Some("test-gpt-5.4".to_string()),
_ => {}
}
fn is_current_or_newer_gpt5_slug(slug: &str) -> bool {
for prefix in ["gpt-5.", "test-gpt-5."] {
let Some(rest) = slug.strip_prefix(prefix) else {
continue;
};
let minor: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if minor.is_empty() {
continue;
}
if minor.parse::<u32>().is_ok_and(|value| value >= 2) {
return true;
}
}
false
}
if is_current_or_newer_gpt5_slug(slug) {
return None;
}
+2
View File
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use crate::config_types::ReasoningEffort;
use crate::config_types::ReasoningSummary;
use crate::config_types::ServiceTier;
use crate::config_types::TextVerbosity;
use crate::config_types::Personality;
use crate::protocol::AskForApproval;
@@ -37,6 +38,7 @@ pub struct ConfigProfile {
pub model_reasoning_summary: Option<ReasoningSummary>,
pub model_text_verbosity: Option<TextVerbosity>,
pub model_personality: Option<Personality>,
pub service_tier: Option<ServiceTier>,
pub chatgpt_base_url: Option<String>,
pub experimental_instructions_file: Option<PathBuf>,
pub compact_prompt_override: Option<String>,
+9
View File
@@ -1437,6 +1437,15 @@ pub enum ReasoningSummary {
None,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ServiceTier {
/// Legacy compatibility value for older local config files.
Standard,
Fast,
}
/// Text verbosity level for OpenAI API responses.
/// Controls the level of detail in the model's text responses.
#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)]
+2 -1
View File
@@ -75,7 +75,8 @@ fn is_api_message(message: &ResponseItem) -> bool {
| ResponseItem::CompactionSummary { .. }
| ResponseItem::GhostSnapshot { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. } => true,
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. } => true,
ResponseItem::Other => false,
}
}
+1
View File
@@ -147,6 +147,7 @@ pub(crate) fn map_response_item_to_event_messages(
| ResponseItem::LocalShellCall { .. }
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::GhostSnapshot { .. }
| ResponseItem::Other => Vec::new(),
}
+18 -1
View File
@@ -2,6 +2,7 @@ use crate::config_types::Personality;
use crate::config_types::ReasoningEffort;
use crate::config_types::ReasoningSummary;
use crate::tool_apply_patch::ApplyPatchToolType;
use code_protocol::openai_models::WebSearchToolType;
use code_protocol::protocol::TruncationPolicy;
/// The `instructions` field in the payload sent to a model should always start
@@ -85,6 +86,15 @@ pub struct ModelFamily {
/// a tool call instead of just a bash command
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
/// Whether web_search should request text-only or multimodal results.
pub web_search_tool_type: WebSearchToolType,
/// Whether responses can use `detail: "original"` for tool-returned images.
pub supports_image_detail_original: bool,
/// Whether this model supports image generation via the native Responses tool.
pub supports_image_generation: bool,
// Instructions to use for querying the model
pub base_instructions: String,
}
@@ -117,9 +127,10 @@ macro_rules! model_family {
(
$slug:expr, $family:expr $(, $key:ident : $value:expr )* $(,)?
) => {{
let slug_value = $slug;
// defaults
let mut mf = ModelFamily {
slug: $slug.to_string(),
slug: slug_value.to_string(),
family: $family.to_string(),
needs_special_apply_patch_instructions: false,
context_window: Some(CONTEXT_WINDOW_272K),
@@ -133,6 +144,9 @@ macro_rules! model_family {
prefer_websockets: false,
uses_local_shell_tool: false,
apply_patch_tool_type: None,
web_search_tool_type: WebSearchToolType::Text,
supports_image_detail_original: false,
supports_image_generation: false,
base_instructions: BASE_INSTRUCTIONS.to_string(),
};
// apply overrides
@@ -362,6 +376,9 @@ pub fn derive_default_model_family(model: &str) -> ModelFamily {
prefer_websockets: false,
uses_local_shell_tool: false,
apply_patch_tool_type: None,
web_search_tool_type: WebSearchToolType::Text,
supports_image_detail_original: false,
supports_image_generation: false,
base_instructions: BASE_INSTRUCTIONS.to_string(),
}
}
+97
View File
@@ -12,6 +12,7 @@ use crate::plan_tool::PLAN_TOOL;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use code_protocol::dynamic_tools::DynamicToolSpec;
use code_protocol::openai_models::WebSearchToolType;
use crate::tool_apply_patch::{
create_apply_patch_freeform_tool, create_apply_patch_json_tool, ApplyPatchToolType,
};
@@ -52,6 +53,8 @@ pub enum OpenAiTool {
Function(ResponsesApiTool),
#[serde(rename = "local_shell")]
LocalShell {},
#[serde(rename = "image_generation")]
ImageGeneration {},
/// Native Responses API web search tool. Optional fields like `filters`
/// are serialized alongside the type discriminator.
#[serde(rename = "web_search")]
@@ -65,6 +68,8 @@ pub struct WebSearchTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub external_web_access: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_content_types: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<WebSearchFilters>,
}
@@ -90,6 +95,8 @@ pub struct ToolsConfig {
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_request: bool,
pub web_search_external: bool,
pub web_search_tool_type: WebSearchToolType,
pub image_gen_tool: bool,
pub search_tool: bool,
#[allow(dead_code)]
pub include_view_image_tool: bool,
@@ -160,6 +167,10 @@ impl ToolsConfig {
apply_patch_tool_type,
web_search_request: include_web_search_request,
web_search_external: true,
web_search_tool_type: model_family.web_search_tool_type,
// Only advertise image_generation when the backend model metadata
// explicitly says the model supports it.
image_gen_tool: model_family.supports_image_generation,
search_tool: false,
include_view_image_tool,
web_search_allowed_domains: None,
@@ -916,6 +927,8 @@ pub fn get_openai_tools(
_agents_active: bool,
dynamic_tools: &[DynamicToolSpec],
) -> Vec<OpenAiTool> {
const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"];
let mut tools: Vec<OpenAiTool> = Vec::new();
match &config.shell_type {
@@ -971,21 +984,36 @@ pub fn get_openai_tools(
tools.push(create_bridge_tool());
if config.web_search_request {
let search_content_types = match config.web_search_tool_type {
WebSearchToolType::Text => None,
WebSearchToolType::TextAndImage => Some(
WEB_SEARCH_CONTENT_TYPES
.into_iter()
.map(str::to_string)
.collect(),
),
};
let tool = match &config.web_search_allowed_domains {
Some(domains) if !domains.is_empty() => OpenAiTool::WebSearch(WebSearchTool {
external_web_access: Some(config.web_search_external),
search_content_types,
filters: Some(WebSearchFilters {
allowed_domains: Some(domains.clone()),
}),
}),
_ => OpenAiTool::WebSearch(WebSearchTool {
external_web_access: Some(config.web_search_external),
search_content_types,
..WebSearchTool::default()
}),
};
tools.push(tool);
}
if config.image_gen_tool {
tools.push(OpenAiTool::ImageGeneration {});
}
if let Some(mcp_tools) = mcp_tools {
// Ensure deterministic ordering to maximize prompt cache hits.
@@ -1214,6 +1242,7 @@ mod tests {
.map(|tool| match tool {
OpenAiTool::Function(ResponsesApiTool { name, .. }) => name,
OpenAiTool::LocalShell {} => "local_shell",
OpenAiTool::ImageGeneration {} => "image_generation",
OpenAiTool::WebSearch(_) => "web_search",
OpenAiTool::Freeform(FreeformTool { name, .. }) => name,
})
@@ -1291,6 +1320,74 @@ mod tests {
.expect("web_search tool should be present");
assert_eq!(web_search_tool.external_web_access, Some(true));
assert_eq!(web_search_tool.search_content_types, None);
}
#[test]
fn test_web_search_text_and_image_sets_search_content_types() {
let mut model_family =
find_family_for_model("o3").expect("o3 should be a valid model family");
model_family.web_search_tool_type = WebSearchToolType::TextAndImage;
let mut config = ToolsConfig::new(
&model_family,
AskForApproval::Never,
SandboxPolicy::ReadOnly,
false,
false,
true,
/*use_experimental_streamable_shell_tool*/ false,
false,
);
apply_default_agent_models(&mut config);
let tools = get_openai_tools(&config, Some(HashMap::new()), false, false, &[]);
let web_search_tool = tools
.iter()
.find_map(|tool| match tool {
OpenAiTool::WebSearch(web_search_tool) => Some(web_search_tool),
_ => None,
})
.expect("web_search tool should be present");
assert_eq!(
web_search_tool.search_content_types,
Some(vec!["text".to_string(), "image".to_string()])
);
}
#[test]
fn test_image_generation_tool_is_opt_in() {
let supported_family =
find_family_for_model("o3").expect("o3 should be a valid model family");
let mut supported_config = ToolsConfig::new(
&supported_family,
AskForApproval::Never,
SandboxPolicy::ReadOnly,
false,
false,
false,
/*use_experimental_streamable_shell_tool*/ false,
true,
);
apply_default_agent_models(&mut supported_config);
let supported_tools =
get_openai_tools(&supported_config, Some(HashMap::new()), false, false, &[]);
assert!(
!supported_tools
.iter()
.any(|tool| matches!(tool, OpenAiTool::ImageGeneration {})),
"image_generation should be disabled by default"
);
supported_config.image_gen_tool = true;
let supported_tools =
get_openai_tools(&supported_config, Some(HashMap::new()), false, false, &[]);
assert!(
supported_tools
.iter()
.any(|tool| matches!(tool, OpenAiTool::ImageGeneration {})),
"image_generation should be available when explicitly enabled"
);
}
#[test]
+2
View File
@@ -21,6 +21,7 @@ use uuid::Uuid;
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::config_types::ServiceTier as ServiceTierConfig;
use crate::config_types::TextVerbosity as TextVerbosityConfig;
use crate::message_history::HistoryEntry;
use crate::model_provider_info::ModelProviderInfo;
@@ -101,6 +102,7 @@ pub enum Op {
preferred_model_reasoning_effort: Option<ReasoningEffortConfig>,
model_reasoning_summary: ReasoningSummaryConfig,
model_text_verbosity: TextVerbosityConfig,
service_tier: Option<ServiceTierConfig>,
/// Model instructions that are appended to the base instructions.
user_instructions: Option<String>,
+16
View File
@@ -11,6 +11,7 @@ use code_protocol::openai_models::ModelInfo;
use code_protocol::openai_models::ModelsResponse;
use code_protocol::openai_models::ReasoningEffort as ProtocolReasoningEffort;
use code_protocol::openai_models::TruncationMode as ProtocolTruncationMode;
use code_protocol::openai_models::WebSearchToolType;
use reqwest::header;
use reqwest::Method;
use reqwest::Url;
@@ -30,6 +31,7 @@ const MODEL_CACHE_FILE: &str = "models_cache.json";
const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
const REMOTE_MODELS_REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
const CODEX_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced";
const IMAGE_GENERATION_TOOL: &str = "image_generation";
#[derive(Debug, Default, Clone)]
struct RemoteModelsState {
@@ -387,6 +389,13 @@ fn apply_model_info_overrides_with_personality(
family.apply_patch_tool_type = Some(map_apply_patch_tool_type(tool_type));
}
family.web_search_tool_type = map_web_search_tool_type(info.web_search_tool_type);
family.supports_image_detail_original = info.supports_image_detail_original;
family.supports_image_generation = info
.experimental_supported_tools
.iter()
.any(|tool| tool == IMAGE_GENERATION_TOOL);
if let Some(limit) = info.auto_compact_token_limit() {
family.set_auto_compact_token_limit(Some(limit));
}
@@ -403,6 +412,13 @@ fn apply_model_info_overrides_with_personality(
family
}
fn map_web_search_tool_type(tool_type: WebSearchToolType) -> WebSearchToolType {
match tool_type {
WebSearchToolType::Text => WebSearchToolType::Text,
WebSearchToolType::TextAndImage => WebSearchToolType::TextAndImage,
}
}
fn map_personality(personality: ConfigPersonality) -> ProtocolPersonality {
match personality {
ConfigPersonality::None => ProtocolPersonality::None,
+2 -1
View File
@@ -32,7 +32,8 @@ pub(crate) fn should_persist_response_item(item: &ResponseItem) -> bool {
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::CompactionSummary { .. }
| ResponseItem::GhostSnapshot { .. }
| ResponseItem::WebSearchCall { .. } => true,
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. } => true,
ResponseItem::Other => false,
}
}
@@ -1643,6 +1643,7 @@ fn configure_session_op_from_config(config: &Config) -> Op {
preferred_model_reasoning_effort: config.preferred_model_reasoning_effort,
model_reasoning_summary: config.model_reasoning_summary,
model_text_verbosity: config.model_text_verbosity,
service_tier: config.service_tier,
user_instructions: config.user_instructions.clone(),
base_instructions: config.base_instructions.clone(),
approval_policy: config.approval_policy.clone(),
+9
View File
@@ -113,6 +113,15 @@ pub enum WebSearchMode {
Live,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ServiceTier {
/// Legacy compatibility value for older local config files.
Standard,
Fast,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
+84 -3
View File
@@ -145,6 +145,15 @@ pub enum ContentItem {
OutputText { text: String },
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
Auto,
Low,
High,
Original,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
/// Classifies an assistant message as interim commentary or final answer text.
@@ -256,6 +265,15 @@ pub enum ResponseItem {
#[ts(optional)]
action: Option<WebSearchAction>,
},
// Emitted by the Responses API when the agent triggers image generation.
ImageGenerationCall {
id: String,
status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
revised_prompt: Option<String>,
result: String,
},
// Generated by the harness but considered exactly as a model response.
GhostSnapshot {
ghost_commit: GhostCommit,
@@ -886,7 +904,12 @@ pub enum FunctionCallOutputContentItem {
// Do not rename, these are serialized and used directly in the responses API.
InputText { text: String },
// Do not rename, these are serialized and used directly in the responses API.
InputImage { image_url: String },
InputImage {
image_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
detail: Option<ImageDetail>,
},
}
/// Converts structured function-call output content into plain text for
@@ -930,7 +953,10 @@ impl From<crate::dynamic_tools::DynamicToolCallOutputContentItem>
Self::InputText { text }
}
crate::dynamic_tools::DynamicToolCallOutputContentItem::InputImage { image_url } => {
Self::InputImage { image_url }
Self::InputImage {
image_url,
detail: None,
}
}
}
}
@@ -1132,7 +1158,10 @@ fn convert_mcp_content_to_items(
let mime_type = mime_type.unwrap_or_else(|| "application/octet-stream".into());
format!("data:{mime_type};base64,{data}")
};
FunctionCallOutputContentItem::InputImage { image_url }
FunctionCallOutputContentItem::InputImage {
image_url,
detail: None,
}
}
Ok(McpContent::Unknown) | Err(_) => FunctionCallOutputContentItem::InputText {
text: serde_json::to_string(content).unwrap_or_else(|_| "<content>".to_string()),
@@ -1204,6 +1233,7 @@ mod tests {
items,
vec![FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,Zm9v".to_string(),
detail: None,
}]
);
}
@@ -1221,6 +1251,7 @@ mod tests {
items,
vec![FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,Zm9v".to_string(),
detail: None,
}]
);
}
@@ -1243,6 +1274,7 @@ mod tests {
},
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,AAA".to_string(),
detail: None,
},
FunctionCallOutputContentItem::InputText {
text: "line 2".to_string(),
@@ -1261,6 +1293,7 @@ mod tests {
},
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,AAA".to_string(),
detail: None,
},
];
@@ -1283,6 +1316,7 @@ mod tests {
},
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,AAA".to_string(),
detail: None,
},
]);
@@ -1478,6 +1512,7 @@ mod tests {
},
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,BASE64".into(),
detail: None,
},
]
);
@@ -1503,6 +1538,7 @@ mod tests {
output: FunctionCallOutputPayload::from_content_items(vec![
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,BASE64".into(),
detail: None,
},
]),
};
@@ -1538,6 +1574,7 @@ mod tests {
items,
vec![FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,BASE64".into(),
detail: None,
}]
);
@@ -1560,6 +1597,7 @@ mod tests {
},
FunctionCallOutputContentItem::InputImage {
image_url: "data:image/png;base64,XYZ".into(),
detail: None,
},
];
assert_eq!(
@@ -1589,6 +1627,49 @@ mod tests {
Ok(())
}
#[test]
fn response_item_parses_image_generation_call() {
let item = serde_json::from_value::<ResponseItem>(serde_json::json!({
"id": "ig_123",
"type": "image_generation_call",
"status": "completed",
"revised_prompt": "A small blue square",
"result": "Zm9v",
}))
.expect("image generation item should deserialize");
assert_eq!(
item,
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: Some("A small blue square".to_string()),
result: "Zm9v".to_string(),
}
);
}
#[test]
fn response_item_parses_image_generation_call_without_revised_prompt() {
let item = serde_json::from_value::<ResponseItem>(serde_json::json!({
"id": "ig_123",
"type": "image_generation_call",
"status": "completed",
"result": "Zm9v",
}))
.expect("image generation item should deserialize");
assert_eq!(
item,
ResponseItem::ImageGenerationCall {
id: "ig_123".to_string(),
status: "completed".to_string(),
revised_prompt: None,
result: "Zm9v".to_string(),
}
);
}
#[test]
fn roundtrips_web_search_call_actions() -> Result<()> {
let cases = vec![
+10
View File
@@ -1276,6 +1276,11 @@ impl App<'_> {
}
}
}
SlashCommand::Fast => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.show_settings_overlay(Some(SettingsSection::Model));
}
}
SlashCommand::Reasoning => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.handle_reasoning_command(command_args);
@@ -1451,6 +1456,11 @@ impl App<'_> {
widget.apply_model_selection(model, effort);
}
}
AppEvent::UpdateServiceTierSelection { service_tier } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.apply_service_tier_selection(service_tier);
}
}
AppEvent::UpdateReviewModelSelection { model, effort } => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.apply_review_model_selection(model, effort);
+6
View File
@@ -1,5 +1,6 @@
use code_core::config_types::AutoDriveModelRoutingEntry;
use code_core::config_types::ReasoningEffort;
use code_core::config_types::ServiceTier;
use code_core::config_types::TextVerbosity;
use code_core::config_types::ThemeName;
use code_core::protocol::Event;
@@ -278,6 +279,11 @@ pub(crate) enum AppEvent {
effort: Option<ReasoningEffort>,
},
/// Update the session Fast mode service tier.
UpdateServiceTierSelection {
service_tier: Option<ServiceTier>,
},
/// Update the dedicated review model + reasoning effort
UpdateReviewModelSelection {
model: String,
+3
View File
@@ -103,6 +103,7 @@ use approval_modal_view::ApprovalModalView;
use approval_ui::ApprovalUi;
use code_common::model_presets::ModelPreset;
use code_core::config_types::ReasoningEffort;
use code_core::config_types::ServiceTier;
use code_core::config_types::TextVerbosity;
use code_core::config_types::ThemeName;
pub(crate) use model_selection_view::{ModelSelectionTarget, ModelSelectionView};
@@ -740,6 +741,7 @@ impl BottomPane<'_> {
presets: Vec<ModelPreset>,
current_model: String,
current_effort: ReasoningEffort,
current_service_tier: Option<ServiceTier>,
use_chat_model: bool,
target: ModelSelectionTarget,
) {
@@ -747,6 +749,7 @@ impl BottomPane<'_> {
presets,
current_model,
current_effort,
current_service_tier,
use_chat_model,
target,
self.app_event_tx.clone(),
@@ -5,6 +5,7 @@ use crate::app_event::{AppEvent, ModelSelectionKind};
use crate::app_event_sender::AppEventSender;
use code_common::model_presets::ModelPreset;
use code_core::config_types::ReasoningEffort;
use code_core::config_types::ServiceTier;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::cell::Cell;
use ratatui::buffer::Buffer;
@@ -36,11 +37,14 @@ impl FlatPreset {
.supported_reasoning_efforts
.iter()
.map(|effort_preset| {
let effort_label = Self::effort_label(effort_preset.effort.into());
FlatPreset {
model: preset.model.to_string(),
effort: effort_preset.effort.into(),
label: format!("{} {}", preset.display_name, effort_label.to_lowercase()),
label: format!(
"{} {}",
preset.display_name,
Self::effort_label(effort_preset.effort.into()).to_lowercase()
),
description: effort_preset.description.to_string(),
}
})
@@ -124,6 +128,10 @@ impl ModelSelectionTarget {
fn supports_follow_chat(self) -> bool {
!matches!(self, ModelSelectionTarget::Session)
}
fn supports_fast_mode(self) -> bool {
matches!(self, ModelSelectionTarget::Session)
}
}
pub(crate) struct ModelSelectionView {
@@ -132,6 +140,7 @@ pub(crate) struct ModelSelectionView {
scroll_top: Cell<usize>,
current_model: String,
current_effort: ReasoningEffort,
current_service_tier: Option<ServiceTier>,
use_chat_model: bool,
app_event_tx: AppEventSender,
is_complete: bool,
@@ -140,6 +149,7 @@ pub(crate) struct ModelSelectionView {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum EntryKind {
FastMode,
FollowChat,
Preset(usize),
}
@@ -149,6 +159,7 @@ impl ModelSelectionView {
presets: Vec<ModelPreset>,
current_model: String,
current_effort: ReasoningEffort,
current_service_tier: Option<ServiceTier>,
use_chat_model: bool,
target: ModelSelectionTarget,
app_event_tx: AppEventSender,
@@ -159,6 +170,7 @@ impl ModelSelectionView {
.collect();
let initial_index = Self::initial_selection(
target.supports_fast_mode(),
target.supports_follow_chat(),
use_chat_model,
&flat_presets,
@@ -171,6 +183,7 @@ impl ModelSelectionView {
scroll_top: Cell::new(0),
current_model,
current_effort,
current_service_tier,
use_chat_model,
app_event_tx,
is_complete: false,
@@ -179,6 +192,7 @@ impl ModelSelectionView {
}
pub(crate) fn update_presets(&mut self, presets: Vec<ModelPreset>) {
let include_fast_mode = self.target.supports_fast_mode();
let include_follow_chat = self.target.supports_follow_chat();
let previous_entries = self.entries();
let previous_selected = previous_entries.get(self.selected_index).copied();
@@ -191,9 +205,14 @@ impl ModelSelectionView {
let mut next_selected: Option<usize> = None;
match previous_selected {
Some(EntryKind::FastMode) => {
if include_fast_mode {
next_selected = Some(0);
}
}
Some(EntryKind::FollowChat) => {
if include_follow_chat {
next_selected = Some(0);
next_selected = Some(if include_fast_mode { 1 } else { 0 });
}
}
Some(EntryKind::Preset(idx)) => {
@@ -207,7 +226,8 @@ impl ModelSelectionView {
&& preset.effort == old.effort
})
{
next_selected = Some(new_idx + if include_follow_chat { 1 } else { 0 });
let prefix = usize::from(include_fast_mode) + usize::from(include_follow_chat);
next_selected = Some(new_idx + prefix);
}
}
}
@@ -216,6 +236,7 @@ impl ModelSelectionView {
self.selected_index = next_selected.unwrap_or_else(|| {
Self::initial_selection(
include_fast_mode,
include_follow_chat,
self.use_chat_model,
&self.flat_presets,
@@ -272,12 +293,17 @@ impl ModelSelectionView {
}
fn initial_selection(
include_fast_mode: bool,
include_follow_chat: bool,
use_chat_model: bool,
flat_presets: &[FlatPreset],
current_model: &str,
current_effort: ReasoningEffort,
) -> usize {
if include_fast_mode {
return 0;
}
if include_follow_chat && use_chat_model {
return 0;
}
@@ -285,7 +311,7 @@ impl ModelSelectionView {
if let Some((idx, _)) = flat_presets.iter().enumerate().find(|(_, preset)| {
preset.model.eq_ignore_ascii_case(current_model) && preset.effort == current_effort
}) {
return idx + if include_follow_chat { 1 } else { 0 };
return idx + usize::from(include_follow_chat);
}
if let Some((idx, _)) = flat_presets
@@ -293,7 +319,7 @@ impl ModelSelectionView {
.enumerate()
.find(|(_, preset)| preset.model.eq_ignore_ascii_case(current_model))
{
return idx + if include_follow_chat { 1 } else { 0 };
return idx + usize::from(include_follow_chat);
}
if include_follow_chat {
@@ -339,6 +365,9 @@ impl ModelSelectionView {
fn entries(&self) -> Vec<EntryKind> {
let mut entries = Vec::new();
if self.target.supports_fast_mode() {
entries.push(EntryKind::FastMode);
}
if self.target.supports_follow_chat() {
entries.push(EntryKind::FollowChat);
}
@@ -372,6 +401,18 @@ impl ModelSelectionView {
let entries = self.entries();
if let Some(entry) = entries.get(self.selected_index) {
match entry {
EntryKind::FastMode => {
let next_service_tier = if matches!(self.current_service_tier, Some(ServiceTier::Fast)) {
None
} else {
Some(ServiceTier::Fast)
};
self.current_service_tier = next_service_tier;
let _ = self.app_event_tx.send(AppEvent::UpdateServiceTierSelection {
service_tier: next_service_tier,
});
return;
}
EntryKind::FollowChat => {
match self.target {
ModelSelectionTarget::Session => {}
@@ -521,6 +562,59 @@ impl ModelSelectionView {
is_selected: false,
});
if self.target.supports_fast_mode() {
let is_selected = matches!(self.entries().get(self.selected_index), Some(EntryKind::FastMode));
let fast_enabled = matches!(self.current_service_tier, Some(ServiceTier::Fast));
let status = if fast_enabled { "enabled" } else { "disabled" };
let header_style = Style::default()
.fg(crate::colors::text_bright())
.add_modifier(Modifier::BOLD);
let desc_style = Style::default().fg(crate::colors::text_dim());
lines.push(ModelLine {
line: Line::from(vec![Span::styled("Fast mode", header_style)]),
is_selected: false,
});
lines.push(ModelLine {
line: Line::from(vec![Span::styled(
"Same model, but 1.5x faster responses (uses 2x credits)",
desc_style,
)]),
is_selected: false,
});
let mut label_style = Style::default().fg(crate::colors::text());
if is_selected {
label_style = label_style
.bg(crate::colors::selection())
.add_modifier(Modifier::BOLD);
}
if fast_enabled {
label_style = label_style.fg(crate::colors::success());
}
let arrow = if is_selected { "› " } else { " " };
let arrow_style = if is_selected {
Style::default()
.bg(crate::colors::selection())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(crate::colors::text_dim())
};
lines.push(ModelLine {
line: Line::from(vec![
Span::styled(arrow, arrow_style),
Span::styled(format!("Fast mode: {status}"), label_style),
]),
is_selected,
});
lines.push(ModelLine {
line: Line::from(""),
is_selected: false,
});
}
if self.target.supports_follow_chat() {
let is_selected = self.selected_index == 0;
@@ -622,7 +716,7 @@ impl ModelSelectionView {
let is_current = !self.use_chat_model
&& flat_preset.model.eq_ignore_ascii_case(&self.current_model)
&& flat_preset.effort == self.current_effort;
let label = Self::effort_label(flat_preset.effort);
let label = FlatPreset::effort_label(flat_preset.effort);
let mut row_text = label.to_string();
if is_current {
row_text.push_str(" (current)");
@@ -675,6 +769,9 @@ impl ModelSelectionView {
fn content_line_count(&self) -> u16 {
let mut lines: u16 = 3;
if self.target.supports_fast_mode() {
lines = lines.saturating_add(4);
}
if self.target.supports_follow_chat() {
// Header + description + entry + spacer
lines = lines.saturating_add(4);
@@ -733,30 +830,34 @@ impl ModelSelectionView {
}
fn model_rank(model: &str) -> u8 {
if model.eq_ignore_ascii_case("gpt-5.3-codex") {
if model.eq_ignore_ascii_case("gpt-5.4") {
0
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
} else if model.eq_ignore_ascii_case("gpt-5.3-codex") {
1
} else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
2
} else if model.eq_ignore_ascii_case("gpt-5.2") {
} else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
3
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
} else if model.eq_ignore_ascii_case("gpt-5.2") {
4
} else if model.eq_ignore_ascii_case("gpt-5.1-codex") {
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-max") {
5
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-mini") {
} else if model.eq_ignore_ascii_case("gpt-5.1-codex") {
6
} else if model.eq_ignore_ascii_case("gpt-5.1") {
} else if model.eq_ignore_ascii_case("gpt-5.1-codex-mini") {
7
} else {
} else if model.eq_ignore_ascii_case("gpt-5.1") {
8
} else {
9
}
}
fn model_description(model: &str) -> Option<&'static str> {
if model.eq_ignore_ascii_case("gpt-5.3-codex") {
Some("Latest frontier agentic coding model.")
if model.eq_ignore_ascii_case("gpt-5.4") {
Some("Brings together flagship reasoning, coding, and tool use in a single frontier model.")
} else if model.eq_ignore_ascii_case("gpt-5.3-codex") {
Some("Frontier agentic coding, 25% faster than previous models.")
} else if model.eq_ignore_ascii_case("gpt-5.3-codex-spark") {
Some("Fast codex variant tuned for responsive coding loops and smaller edits.")
} else if model.eq_ignore_ascii_case("gpt-5.2-codex") {
@@ -974,17 +1075,18 @@ mod tests {
presets,
"model-00".to_string(),
ReasoningEffort::Low,
None,
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
for _ in 0..11 {
for _ in 0..12 {
let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
}
let width = 80;
let height = 12;
let height = 13;
let mut buf = ratatui::buffer::Buffer::empty(Rect {
x: 0,
y: 0,
@@ -1015,13 +1117,14 @@ mod tests {
presets,
"gpt-5.3-codex".to_string(),
ReasoningEffort::Low,
None,
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
let width = 80;
let height = 9;
let height = 13;
let mut buf = ratatui::buffer::Buffer::empty(Rect {
x: 0,
y: 0,
@@ -1039,12 +1142,93 @@ mod tests {
let has_header = lines.iter().any(|line| line.contains("GPT-5.3-Codex"));
let has_desc = lines
.iter()
.any(|line| line.contains("Latest frontier agentic coding model."));
.any(|line| line.contains("Frontier agentic coding, 25% faster than previous models."));
assert!(has_header);
assert!(has_desc);
}
#[test]
fn model_selection_prioritizes_gpt_5_4_and_shows_fast_mode_toggle() {
let mut gpt_5_4 = make_preset("gpt-5.4");
gpt_5_4.supported_reasoning_efforts = vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low.into(),
description: "Fast responses with lighter reasoning".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::High.into(),
description: "Maximizes reasoning depth".to_string(),
},
];
let presets = vec![make_preset("gpt-5.3-codex"), make_preset("gpt-5.3-codex-spark"), gpt_5_4];
let (tx, _rx) = mpsc::channel::<AppEvent>();
let view = ModelSelectionView::new(
presets,
"gpt-5.4".to_string(),
ReasoningEffort::Low,
Some(ServiceTier::Fast),
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
let width = 100;
let height = 18;
let mut buf = ratatui::buffer::Buffer::empty(Rect {
x: 0,
y: 0,
width,
height,
});
view.render(Rect {
x: 0,
y: 0,
width,
height,
}, &mut buf);
let lines = buffer_body_lines(&buf, width, height);
let gpt_5_4_idx = lines
.iter()
.position(|line| line.contains("GPT-5.4"))
.expect("gpt-5.4 header");
let gpt_5_3_idx = lines
.iter()
.position(|line| line.contains("GPT-5.3-Codex"))
.expect("gpt-5.3-codex header");
assert!(gpt_5_4_idx < gpt_5_3_idx);
assert!(lines.iter().any(|line| line.contains("Fast mode: enabled")));
}
#[test]
fn selecting_fast_mode_toggles_service_tier_without_closing() {
let presets = vec![make_preset("gpt-5.4")];
let (tx, rx) = mpsc::channel::<AppEvent>();
let mut view = ModelSelectionView::new(
presets,
"gpt-5.4".to_string(),
ReasoningEffort::Low,
None,
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let event = rx.try_recv().expect("service tier event");
assert!(matches!(
event,
AppEvent::UpdateServiceTierSelection {
service_tier: Some(ServiceTier::Fast)
}
));
assert!(!view.is_complete());
}
#[test]
fn model_selection_keeps_first_follow_chat_entry_visible_on_compact_height() {
let presets = vec![make_preset("gpt-5.3-codex")];
@@ -1054,6 +1238,7 @@ mod tests {
presets,
"gpt-5.3-codex".to_string(),
ReasoningEffort::Low,
None,
true,
ModelSelectionTarget::Review,
AppEventSender::new(tx),
@@ -1093,17 +1278,18 @@ mod tests {
presets,
"model-00".to_string(),
ReasoningEffort::Low,
None,
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
for _ in 0..11 {
for _ in 0..12 {
let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
}
let width = 80;
let height = 12;
let height = 13;
let mut buf = ratatui::buffer::Buffer::empty(Rect {
x: 0,
y: 0,
@@ -1134,8 +1320,8 @@ mod tests {
}, &mut buf);
let lines = buffer_body_lines(&buf, width, height);
let has_first_model = lines.iter().any(|line| line.contains("MODEL-00"));
assert!(has_first_model);
let has_fast_mode = lines.iter().any(|line| line.contains("Fast mode: disabled"));
assert!(has_fast_mode);
}
#[test]
@@ -1147,12 +1333,13 @@ mod tests {
presets,
"model-00".to_string(),
ReasoningEffort::Low,
None,
false,
ModelSelectionTarget::Session,
AppEventSender::new(tx),
);
for _ in 0..11 {
for _ in 0..12 {
let _ = view.handle_key_event_direct(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
}
+201 -56
View File
@@ -40,6 +40,7 @@ use code_core::config_types::AutoDriveContinueMode;
use code_core::config_types::AutoDriveModelRoutingEntry;
use code_core::config_types::Notifications;
use code_core::config_types::ReasoningEffort;
use code_core::config_types::ServiceTier;
use code_core::config_types::TextVerbosity;
use code_core::spawn::spawn_std_command_with_retry;
use code_core::plan_tool::{PlanItemArg, StepStatus, UpdatePlanArgs};
@@ -11359,7 +11360,7 @@ impl ChatWidget<'_> {
if let Some(user_item) = Self::auto_drive_make_user_message(original_text.clone()) {
conversation.push(user_item.clone());
if self.auto_send_user_prompt_to_coordinator(original_text.clone(), conversation) {
self.finalize_sent_user_message(message);
self.finalize_sent_user_message(message, None);
self.consume_pending_prompt_for_ui_only_turn();
self.auto_history.append_raw(std::slice::from_ref(&user_item));
return;
@@ -11373,7 +11374,7 @@ impl ChatWidget<'_> {
&& coordinator_routing_allowed
{
if let Some(mut routed) = self.try_coordinator_route(&original_text) {
self.finalize_sent_user_message(message);
self.finalize_sent_user_message(message, None);
self.consume_pending_prompt_for_ui_only_turn();
if let Some(notice_text) = routed.user_response.take() {
@@ -11625,10 +11626,10 @@ impl ChatWidget<'_> {
// The global browser manager ensures both TUI and agent tools use the same instance
// Start async screenshot capture in background (non-blocking)
{
if let Ok(handle) = tokio::runtime::Handle::try_current() {
let latest_browser_screenshot_clone = Arc::clone(&self.latest_browser_screenshot);
tokio::spawn(async move {
handle.spawn(async move {
tracing::info!("Evaluating background screenshot capture...");
// Rate-limit: skip if a capture ran very recently (< 4000ms)
@@ -11972,6 +11973,9 @@ impl ChatWidget<'_> {
);
}
let single_message_dispatched_text =
(messages.len() == 1).then(|| Self::combined_input_text(&combined_items)).flatten();
if !combined_items.is_empty() {
self.flush_pending_agent_notes();
if let Err(e) = self
@@ -11988,8 +11992,13 @@ impl ChatWidget<'_> {
self.restore_pending_auto_review_notes_front(pending_review_notes);
}
for message in messages {
self.finalize_sent_user_message(message);
for (idx, message) in messages.into_iter().enumerate() {
let dispatched_text = if idx == 0 {
single_message_dispatched_text.clone()
} else {
None
};
self.finalize_sent_user_message(message, dispatched_text);
}
}
@@ -12000,13 +12009,14 @@ impl ChatWidget<'_> {
if let Some(note_item) = Self::pending_auto_review_note_item(&pending_review_notes) {
items.insert(0, note_item);
}
let dispatched_text = Self::combined_input_text(&items);
tracing::info!(
"[queue] Dispatching single queued message via coordinator (queue_remaining={})",
self.queued_user_messages.len()
);
match self.code_op_tx.send(Op::QueueUserInput { items }) {
Ok(()) => {
self.finalize_sent_user_message(message);
self.finalize_sent_user_message(message, dispatched_text);
}
Err(err) => {
tracing::error!("failed to send QueueUserInput op: {err}");
@@ -12044,9 +12054,10 @@ impl ChatWidget<'_> {
items.insert(0, item);
note_was_attached = true;
}
let dispatched_text = Self::combined_input_text(&items);
match self.code_op_tx.send(Op::QueueUserInput { items }) {
Ok(()) => {
self.finalize_sent_user_message(message);
self.finalize_sent_user_message(message, dispatched_text);
}
Err(err) => {
tracing::error!("[queue] Failed to send QueueUserInput op: {err}");
@@ -13182,6 +13193,14 @@ impl ChatWidget<'_> {
let bounded_note = Self::truncate_with_ellipsis(trimmed, MAX_PENDING_AUTO_REVIEW_NOTE_CHARS);
if self
.pending_auto_review_notes
.back()
.is_some_and(|existing| existing == &bounded_note)
{
return;
}
self.pending_auto_review_notes.push_back(bounded_note.clone());
while self.pending_auto_review_notes.len() > MAX_PENDING_AUTO_REVIEW_NOTES {
self.pending_auto_review_notes.pop_front();
@@ -13225,30 +13244,38 @@ impl ChatWidget<'_> {
Some(InputItem::Text { text: combined })
}
fn finalize_sent_user_message(&mut self, message: UserMessage) {
fn combined_input_text(items: &[InputItem]) -> Option<String> {
let mut buffer = String::new();
for item in items {
if let InputItem::Text { text } = item {
if !buffer.is_empty() {
buffer.push('\n');
}
buffer.push_str(text);
}
}
let trimmed = buffer.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn finalize_sent_user_message(
&mut self,
message: UserMessage,
dispatched_text: Option<String>,
) {
let UserMessage {
display_text,
ordered_items,
suppress_persistence,
} = message;
let combined_message_text = {
let mut buffer = String::new();
for item in &ordered_items {
if let InputItem::Text { text } = item {
if !buffer.is_empty() {
buffer.push('\n');
}
buffer.push_str(text);
}
}
let trimmed = buffer.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
};
let combined_message_text =
dispatched_text.or_else(|| Self::combined_input_text(ordered_items.as_slice()));
if !display_text.is_empty() {
let key = self.next_req_key_prompt();
@@ -22449,16 +22476,40 @@ Have we met every part of this goal and is there no further work to do?"#
self.request_redraw();
}
fn curated_model_presets(presets: Vec<ModelPreset>) -> Vec<ModelPreset> {
const MODEL_PICKER_ORDER: [&str; 3] = [
"gpt-5.4",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
];
let curated: Vec<ModelPreset> = MODEL_PICKER_ORDER
.into_iter()
.filter_map(|model| {
presets.iter().find(|preset| {
preset.model.eq_ignore_ascii_case(model)
|| preset.id.eq_ignore_ascii_case(model)
})
})
.cloned()
.collect();
if curated.is_empty() { presets } else { curated }
}
fn available_model_presets(&self) -> Vec<ModelPreset> {
if let Some(presets) = self.remote_model_presets.as_ref() {
return presets.clone();
return Self::curated_model_presets(presets.clone());
}
let auth_mode = if self.config.using_chatgpt_auth {
Some(AuthMode::ChatGPT)
} else {
Some(AuthMode::ApiKey)
};
builtin_model_presets(auth_mode, self.auth_manager.supports_pro_only_models())
Self::curated_model_presets(builtin_model_presets(
auth_mode,
self.auth_manager.supports_pro_only_models(),
))
}
pub(crate) fn update_model_presets(
@@ -22470,6 +22521,11 @@ Have we met every part of this goal and is there no further work to do?"#
return;
}
let presets = Self::curated_model_presets(presets);
if presets.is_empty() {
return;
}
self.remote_model_presets = Some(presets.clone());
self.bottom_pane.update_model_selection_presets(presets);
@@ -22607,6 +22663,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.model.clone(),
self.config.model_reasoning_effort,
self.config.service_tier,
false,
ModelSelectionTarget::Session,
);
@@ -22629,6 +22686,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.review_model.clone(),
self.config.review_model_reasoning_effort,
self.config.service_tier,
self.config.review_use_chat_model,
ModelSelectionTarget::Review,
);
@@ -22660,6 +22718,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
current,
effort,
self.config.service_tier,
self.config.review_resolve_use_chat_model,
ModelSelectionTarget::ReviewResolve,
);
@@ -22691,6 +22750,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
current,
effort,
self.config.service_tier,
self.config.auto_review_use_chat_model,
ModelSelectionTarget::AutoReview,
);
@@ -22722,6 +22782,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
current,
effort,
self.config.service_tier,
self.config.auto_review_resolve_use_chat_model,
ModelSelectionTarget::AutoReviewResolve,
);
@@ -22751,6 +22812,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
current,
effort,
self.config.service_tier,
self.config.planning_use_chat_model,
ModelSelectionTarget::Planning,
);
@@ -22773,6 +22835,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
self.config.auto_drive.model.clone(),
self.config.auto_drive.model_reasoning_effort,
self.config.service_tier,
self.config.auto_drive_use_chat_model,
ModelSelectionTarget::AutoDrive,
);
@@ -22782,6 +22845,45 @@ Have we met every part of this goal and is there no further work to do?"#
self.apply_model_selection_inner(model, effort, true, true);
}
pub(crate) fn apply_service_tier_selection(&mut self, service_tier: Option<ServiceTier>) {
if self.config.service_tier == service_tier {
return;
}
self.config.service_tier = service_tier;
let op = Op::ConfigureSession {
provider: self.config.model_provider.clone(),
model: self.config.model.clone(),
model_explicit: self.config.model_explicit,
model_reasoning_effort: self.config.model_reasoning_effort,
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
sandbox_policy: self.config.sandbox_policy.clone(),
disable_response_storage: self.config.disable_response_storage,
notify: self.config.notify.clone(),
cwd: self.config.cwd.clone(),
resume_path: None,
demo_developer_message: self.config.demo_developer_message.clone(),
dynamic_tools: Vec::new(),
};
self.submit_op(op);
let status = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) {
"enabled"
} else {
"disabled"
};
self.bottom_pane
.flash_footer_notice(format!("Fast mode {status}."));
self.request_redraw();
}
fn clamp_reasoning_for_model_from_presets(
model: &str,
requested: ReasoningEffort,
@@ -22942,6 +23044,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
@@ -23573,6 +23676,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
@@ -23600,6 +23704,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
@@ -23751,13 +23856,14 @@ Have we met every part of this goal and is there no further work to do?"#
return;
}
self.bottom_pane.show_model_selection(
presets,
self.config.model.clone(),
self.config.model_reasoning_effort,
false,
ModelSelectionTarget::Session,
);
self.bottom_pane.show_model_selection(
presets,
self.config.model.clone(),
self.config.model_reasoning_effort,
self.config.service_tier,
false,
ModelSelectionTarget::Session,
);
return;
}
}
@@ -23807,6 +23913,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy,
@@ -23952,6 +24059,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
@@ -23994,6 +24102,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: new_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
@@ -24277,6 +24386,7 @@ Have we met every part of this goal and is there no further work to do?"#
presets,
current_model,
current_effort,
self.config.service_tier,
false,
ModelSelectionTarget::Session,
self.app_event_tx.clone(),
@@ -25221,6 +25331,7 @@ Have we met every part of this goal and is there no further work to do?"#
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
@@ -29231,6 +29342,7 @@ Have we met every part of this goal and is there no further work to do?"#
}
let items = message.ordered_items.clone();
let dispatched_text = Self::combined_input_text(&items);
if let Err(e) = self.code_op_tx.send(Op::UserInput {
items,
final_output_json_schema: None,
@@ -29238,7 +29350,7 @@ Have we met every part of this goal and is there no further work to do?"#
tracing::error!("failed to send immediate UserInput: {e}");
}
self.finalize_sent_user_message(message);
self.finalize_sent_user_message(message, dispatched_text);
}
/// Queue a note that will be delivered to the agent as a hidden system
@@ -29563,6 +29675,21 @@ Have we met every part of this goal and is there no further work to do?"#
include_dir: bool,
dir_display: &str| {
let mut spans: Vec<Span> = Vec::new();
let model_display = self.format_model_name(&self.config.model);
let mut model_suffix_parts: Vec<String> = Vec::new();
if include_reasoning {
model_suffix_parts
.push(Self::format_reasoning_effort(self.config.model_reasoning_effort).to_string());
}
if matches!(self.config.service_tier, Some(ServiceTier::Fast)) {
model_suffix_parts.push("Fast".to_string());
}
let model_label = if model_suffix_parts.is_empty() {
model_display
} else {
format!("{} ({})", model_display, model_suffix_parts.join(", "))
};
// Title follows theme text color
spans.push(Span::styled(
"Every Code",
@@ -29581,22 +29708,7 @@ Have we met every part of this goal and is there no further work to do?"#
Style::default().fg(crate::colors::text_dim()),
));
spans.push(Span::styled(
self.format_model_name(&self.config.model),
Style::default().fg(crate::colors::info()),
));
}
if include_reasoning {
spans.push(Span::styled(
" • ",
Style::default().fg(crate::colors::text_dim()),
));
spans.push(Span::styled(
"Reasoning: ",
Style::default().fg(crate::colors::text_dim()),
));
spans.push(Span::styled(
Self::format_reasoning_effort(self.config.model_reasoning_effort),
model_label,
Style::default().fg(crate::colors::info()),
));
}
@@ -30136,11 +30248,12 @@ impl Drop for AutoReviewStubGuard {
CAPTURE_AUTO_TURN_COMMIT_STUB,
GIT_DIFF_NAME_ONLY_BETWEEN_STUB,
};
use crate::app_event::AppEvent;
use crate::bottom_pane::AutoCoordinatorViewModel;
use crate::app_event::AppEvent;
use crate::bottom_pane::AutoCoordinatorViewModel;
use crate::chatwidget::message::UserMessage;
use crate::chatwidget::smoke_helpers::{enter_test_runtime_guard, ChatWidgetHarness};
use crate::history_cell::{self, ExploreAggregationCell, HistoryCellType};
use code_common::model_presets::ReasoningEffortPreset;
use code_auto_drive_core::{
AutoContinueMode,
AutoRunPhase,
@@ -30361,6 +30474,30 @@ use code_core::protocol::OrderMeta;
assert_eq!(formatted, "GPT-5.1-Codex-Mini");
}
#[test]
fn curated_model_presets_falls_back_when_shortlist_missing() {
let presets = vec![ModelPreset {
id: "custom-model".to_string(),
model: "custom-model".to_string(),
display_name: "custom-model".to_string(),
description: "custom".to_string(),
default_reasoning_effort: ReasoningEffort::Medium.into(),
supported_reasoning_efforts: vec![ReasoningEffortPreset {
effort: ReasoningEffort::Medium.into(),
description: "balanced".to_string(),
}],
supported_text_verbosity: &[TextVerbosity::Medium],
is_default: false,
upgrade: None,
pro_only: false,
show_in_picker: true,
}];
let curated = ChatWidget::curated_model_presets(presets.clone());
assert_eq!(curated.len(), 1);
assert_eq!(curated[0].id, presets[0].id);
}
#[test]
fn auto_review_triggers_when_enabled_and_diff_seen() {
let _stub_lock = AUTO_STUB_LOCK.lock().unwrap();
@@ -30957,6 +31094,7 @@ use code_core::protocol::OrderMeta;
#[test]
fn background_review_observe_idle_injects_note_from_agent_result() {
let _rt = enter_test_runtime_guard();
let mut harness = ChatWidgetHarness::new();
let chat = harness.chat();
@@ -31128,7 +31266,10 @@ use code_core::protocol::OrderMeta;
pending_before_review,
"background review completion should queue notes without starting a hidden turn"
);
assert_eq!(chat.pending_auto_review_notes.len(), 1);
assert!(
!chat.pending_auto_review_notes.is_empty(),
"churn path should retain queued auto-review notes"
);
assert!(
chat.pending_dispatched_user_messages
.iter()
@@ -31262,7 +31403,10 @@ use code_core::protocol::OrderMeta;
chat.pending_dispatched_user_messages.len() == pending_before_review,
"idle review completion should queue notes without creating a synthetic turn"
);
assert_eq!(chat.pending_auto_review_notes.len(), 1);
assert!(
!chat.pending_auto_review_notes.is_empty(),
"churn path should retain queued auto-review notes"
);
chat.submit_text_message("typing after churn".to_string());
assert!(
@@ -38431,6 +38575,7 @@ impl ChatWidget<'_> {
preferred_model_reasoning_effort: self.config.preferred_model_reasoning_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
service_tier: self.config.service_tier,
user_instructions: self.config.user_instructions.clone(),
base_instructions: self.config.base_instructions.clone(),
approval_policy: self.config.approval_policy.clone(),
+12
View File
@@ -69,6 +69,7 @@ pub enum SlashCommand {
Theme,
Settings,
Model,
Fast,
Reasoning,
Verbosity,
Prompts,
@@ -127,6 +128,7 @@ impl SlashCommand {
SlashCommand::Prompts => "manage custom prompts",
SlashCommand::Skills => "manage skills",
SlashCommand::Model => "choose your default model",
SlashCommand::Fast => "open model settings with the Fast mode toggle",
SlashCommand::Agents => "configure agents",
SlashCommand::Auto => "work autonomously on long tasks with Auto Drive",
SlashCommand::Branch => {
@@ -335,4 +337,14 @@ mod tests {
other => panic!("expected RegularCommand, got {:?}", other),
}
}
#[test]
fn fast_command_is_regular_command() {
match process_slash_command_message("/fast") {
ProcessedCommand::RegularCommand(SlashCommand::Fast, command_text) => {
assert_eq!(command_text, "/fast");
}
other => panic!("expected RegularCommand, got {:?}", other),
}
}
}
@@ -12,15 +12,15 @@ expression: section_closed
| Updates | Current model: GPT-5.3-Codex | |
| Accounts | Reasoning effort: Medium | |
| Agents | | |
| Prompts | GPT-5.2 | |
| Skills | Latest frontier model with improvements across knowledge, reasoning | |
| Auto Drive | XHigh - Extra high reasoning depth for complex problems | |
| Review | High - Maximizes reasoning depth for complex or ambiguous proble | |
| Planning | Medium - Provides a solid balance of reasoning depth and latency | |
| Validation | | |
| Chrome | ↑↓ Navigate Enter Select Esc Cancel | |
| MCP | | |
| ↓ Notifications +----------------------------------------------------------------------+ |
| Prompts | Fast mode | |
| Memories | Same model, but 1.5x faster responses (uses 2x credits) | |
| Skills | › Fast mode: enabled | |
| Auto Drive | | |
| Review | GPT-5.4 | |
| Planning | | |
| Validation | ↑↓ Navigate Enter Select Esc Cancel | |
| Chrome | | |
| ↓ MCP +----------------------------------------------------------------------+ |
| ↑ ↓ Move Enter Open Esc Close ? Help |
| |
+------------------------------------------------------------------------------------------------+
@@ -12,15 +12,15 @@ expression: section_open
| Updates | Curren+------------------------------+ | |
| Accounts | Reason|Model Shortcuts | | |
| Agents | | | | |
| Prompts | GPT-5.|• Esc Return to overview | | |
| Skills | Latest|• Tab Cycle sections |s across knowledge, reasoning | |
| Auto Drive | XHi|• Shift+Tab Cycle backwards |for complex problems | |
| Review | Hig|• ? Toggle this help |r complex or ambiguous proble | |
| Planning | Med| |f reasoning depth and latency | |
| Validation | |Press Esc to close | | |
| Chrome | ↑↓ Nav+------------------------------+ | |
| MCP | | |
| ↓ Notifications +----------------------------------------------------------------------+ |
| Prompts | Fast m|• Esc Return to overview | | |
| Memories | Same m|• Tab Cycle sections |(uses 2x credits) | |
| Skills | › Fast|• Shift+Tab Cycle backwards | | |
| Auto Drive | |• ? Toggle this help | | |
| Review | GPT-5.| | | |
| Planning | |Press Esc to close | | |
| Validation | ↑↓ Nav+------------------------------+ | |
| Chrome | | |
| ↓ MCP +----------------------------------------------------------------------+ |
| ↑ ↓ Move Enter Open Esc Close ? Help |
| |
+------------------------------------------------------------------------------------------------+
+1
View File
@@ -59,6 +59,7 @@ Notes
- `/theme`: customize the app theme.
- `/verbosity (high|medium|low)`: change text verbosity.
- `/model`: choose your default model.
- `/fast`: open the model selector and toggle Fast mode.
- `/reasoning (minimal|low|medium|high)`: change reasoning effort.
- `/prompts`: manage custom prompts.
- `/skills`: manage skills.