mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(models): add GPT-5.4 fast mode support
This commit is contained in:
@@ -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": [
|
||||
|
||||
+54
-1
@@ -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
-2
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -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 |
|
||||
| |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
|
||||
+9
-9
@@ -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 |
|
||||
| |
|
||||
+------------------------------------------------------------------------------------------------+
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user