Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/ui/app/src/components/CodexDesktopModelsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ export const CodexDesktopModelsSettings = forwardRef<
<Popover className="relative w-full">
<div
data-testid="chatgpt-model-picker"
className="relative flex min-h-10 w-full flex-wrap items-center gap-1.5 rounded-lg bg-neutral-50 px-2 py-1.5 ring-1 ring-inset ring-neutral-200 hover:bg-neutral-100 dark:bg-neutral-700 dark:ring-neutral-600 dark:hover:bg-neutral-600"
className="relative flex min-h-10 w-full flex-wrap items-center gap-2 rounded-lg bg-neutral-50 p-2 ring-1 ring-inset ring-neutral-200 hover:bg-neutral-100 dark:bg-neutral-700 dark:ring-neutral-600 dark:hover:bg-neutral-600"
>
<PopoverButton
aria-label="Add ChatGPT model"
Expand All @@ -575,7 +575,7 @@ export const CodexDesktopModelsSettings = forwardRef<
>
<span className="sr-only">Choose ChatGPT models</span>
</PopoverButton>
<div className="pointer-events-none relative z-10 flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
<div className="pointer-events-none relative z-10 flex min-w-0 flex-1 flex-wrap items-center gap-2">
{selected.map((model) => (
<span
key={model}
Expand Down
43 changes: 30 additions & 13 deletions openai/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,15 @@ type ResponsesFunctionCall struct {

func (ResponsesFunctionCall) responsesInputItem() {}

// ResponsesFunctionCallOutput represents a function call result from the client.
// ResponsesFunctionCallOutput represents a paired result or standalone named
// output from the client.
type ResponsesFunctionCallOutput struct {
Type string `json:"type"` // always "function_call_output"
CallID string `json:"call_id"` // links to the original function call
Output string `json:"output"` // the function result
ID string `json:"id,omitempty"`
Type string `json:"type"` // always "function_call_output"
CallID string `json:"call_id,omitempty"` // links to the original function call, if any
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
Output string `json:"output"`

// OutputItems is populated when output is provided as Responses content
// items instead of the string shorthand.
Expand All @@ -194,18 +198,27 @@ type ResponsesFunctionCallOutput struct {

func (o *ResponsesFunctionCallOutput) UnmarshalJSON(data []byte) error {
var aux struct {
Type string `json:"type"`
CallID string `json:"call_id"`
Output json.RawMessage `json:"output"`
ID string `json:"id"`
Type string `json:"type"`
CallID *string `json:"call_id"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Output json.RawMessage `json:"output"`
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}

o.Type = aux.Type
o.CallID = aux.CallID
o.Output = ""
o.OutputItems = nil
if aux.CallID != nil && strings.TrimSpace(*aux.CallID) == "" {
return errors.New("function output call_id must not be empty")
}
if aux.CallID == nil && strings.TrimSpace(aux.Name) == "" {
return errors.New("standalone function output is missing name")
}
*o = ResponsesFunctionCallOutput{ID: aux.ID, Type: aux.Type, Name: aux.Name, Namespace: aux.Namespace}
if aux.CallID != nil {
o.CallID = *aux.CallID
}

if len(aux.Output) == 0 {
return nil
Expand Down Expand Up @@ -654,12 +667,16 @@ func FromResponsesRequest(r ResponsesRequest) (*api.ChatRequest, error) {
return nil, err
}
}
messages = append(messages, api.Message{
message := api.Message{
Role: "tool",
Content: content,
Images: images,
ToolCallID: v.CallID,
})
}
if v.CallID == "" {
message.ToolName = qualifyNamespaceToolName(v.Namespace, v.Name)
}
messages = append(messages, message)
case ResponsesToolSearchCall:
messages = appendResponseToolCall(messages, api.ToolCall{
ID: v.CallID,
Expand Down
89 changes: 70 additions & 19 deletions openai/responses_compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,23 @@ type OllamaCompactionPayload struct {
Version int `json:"version"`
Summary string `json:"summary"`
Retained []api.Message `json:"retained"`
// StandaloneNames preserves Responses identities by retained-message index.
// Qualified native names alone cannot distinguish every namespace/member pair.
StandaloneNames map[int]compactionFunctionName `json:"standalone_names,omitempty"`
}

type compactionFunctionName struct {
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
}

// CompactionTranscriptItem is one ordered input item shown to the compaction
// model. Ref is request-local and is the only value the model may select.
type CompactionTranscriptItem struct {
Ref string `json:"ref"`
Type string `json:"type"`
Message api.Message `json:"message"`
Ref string `json:"ref"`
Type string `json:"type"`
Message api.Message `json:"message"`
StandaloneName *compactionFunctionName `json:"standalone_name,omitempty"`
}

type compactionToolMetadata struct {
Expand All @@ -65,10 +74,11 @@ type compactionToolMetadata struct {
}

type compactionTranscriptItemWire struct {
Ref string `json:"ref"`
Type string `json:"type"`
Message api.Message `json:"message"`
ImageCount int `json:"image_count,omitempty"`
Ref string `json:"ref"`
Type string `json:"type"`
Message api.Message `json:"message"`
StandaloneName *compactionFunctionName `json:"standalone_name,omitempty"`
ImageCount int `json:"image_count,omitempty"`
}

type compactionToolGroup struct {
Expand Down Expand Up @@ -300,6 +310,15 @@ func decodeOllamaCompactionItem(item json.RawMessage) (OllamaCompactionPayload,
}

func payloadToResponsesItems(payload OllamaCompactionPayload) ([]json.RawMessage, error) {
for index, name := range payload.StandaloneNames {
if index < 0 || index >= len(payload.Retained) {
return nil, fmt.Errorf("standalone name refers to invalid retained-message index %d", index)
}
message := payload.Retained[index]
if message.Role != "tool" || message.ToolCallID != "" || strings.TrimSpace(name.Name) == "" || qualifyNamespaceToolName(name.Namespace, name.Name) != message.ToolName {
return nil, fmt.Errorf("standalone name does not match retained message %d", index)
}
}
b, err := json.Marshal(payload)
if err != nil {
return nil, err
Expand All @@ -322,8 +341,8 @@ func payloadToResponsesItems(payload OllamaCompactionPayload) ([]json.RawMessage
return nil, err
}
items = append(items, call, result)
for _, message := range payload.Retained {
converted, err := messageToResponsesItems(message)
for i, message := range payload.Retained {
converted, err := messageToResponsesItems(message, payload.StandaloneNames[i])
if err != nil {
return nil, fmt.Errorf("invalid retained message: %w", err)
}
Expand All @@ -332,7 +351,7 @@ func payloadToResponsesItems(payload OllamaCompactionPayload) ([]json.RawMessage
return items, nil
}

func messageToResponsesItems(message api.Message) ([]json.RawMessage, error) {
func messageToResponsesItems(message api.Message, standaloneName compactionFunctionName) ([]json.RawMessage, error) {
var values []any
if message.Thinking != "" {
values = append(values, map[string]any{
Expand All @@ -342,9 +361,19 @@ func messageToResponsesItems(message api.Message) ([]json.RawMessage, error) {
}
if message.Role == "tool" {
if message.ToolCallID == "" {
return nil, errors.New("retained tool message is missing tool_call_id")
}
if message.ToolName == "tool_search" {
if strings.TrimSpace(standaloneName.Name) == "" {
return nil, errors.New("retained tool message is missing tool_call_id or standalone name")
}
output, err := responsesContentValue(message.Content, message.Images)
if err != nil {
return nil, err
}
value := map[string]any{"type": "function_call_output", "name": standaloneName.Name, "output": output}
if standaloneName.Namespace != "" {
value["namespace"] = standaloneName.Namespace
}
values = append(values, value)
} else if message.ToolName == "tool_search" {
if len(message.Images) > 0 {
return nil, errors.New("retained tool search output cannot contain images")
}
Expand Down Expand Up @@ -455,9 +484,13 @@ func newResponsesCompactionPlan(req rawResponsesRequest, rawItems []json.RawMess
if err != nil {
return nil, fmt.Errorf("input[%d]: %w", i, err)
}
items = append(items, CompactionTranscriptItem{
entry := CompactionTranscriptItem{
Ref: fmt.Sprintf("item_%06d", i+1), Type: kind, Message: message,
})
}
if output, ok := item.(ResponsesFunctionCallOutput); ok && output.CallID == "" {
entry.StandaloneName = &compactionFunctionName{Name: output.Name, Namespace: output.Namespace}
}
items = append(items, entry)
}

groups, forced, err := analyzeCompactionToolState(items)
Expand Down Expand Up @@ -502,7 +535,11 @@ func compactionMessage(item ResponsesInputItem) (api.Message, string, error) {
return api.Message{}, "", err
}
}
return api.Message{Role: "tool", Content: content, Images: images, ToolCallID: value.CallID}, "function_call_output", nil
message := api.Message{Role: "tool", Content: content, Images: images, ToolCallID: value.CallID}
if value.CallID == "" {
message.ToolName = qualifyNamespaceToolName(value.Namespace, value.Name)
}
return message, "function_call_output", nil
case ResponsesToolSearchCall:
return api.Message{Role: "assistant", ToolCalls: []api.ToolCall{{
ID: value.CallID, Function: api.ToolCallFunction{Name: "tool_search", Arguments: value.Arguments},
Expand Down Expand Up @@ -571,6 +608,7 @@ func analyzeCompactionToolState(items []CompactionTranscriptItem) ([]compactionT
}
byCallID := make(map[string]*pendingGroup)
ignoredCallIDs := make(map[string]struct{})
forced := make(map[string]struct{})
var ordered []*pendingGroup

for i, item := range items {
Expand All @@ -592,6 +630,12 @@ func analyzeCompactionToolState(items []CompactionTranscriptItem) ([]compactionT
ordered = append(ordered, group)
case "function_call_output":
callID := item.Message.ToolCallID
if callID == "" && item.StandaloneName != nil {
// Standalone outputs can carry the task instructions. Retain them
// without inventing a call or relying on the summary to repeat them.
forced[item.Ref] = struct{}{}
continue
}
if _, ignored := ignoredCallIDs[callID]; ignored {
continue
}
Expand All @@ -607,7 +651,6 @@ func analyzeCompactionToolState(items []CompactionTranscriptItem) ([]compactionT
}
}

forced := make(map[string]struct{})
groups := make([]compactionToolGroup, 0, len(ordered))
for _, candidate := range ordered {
groups = append(groups, candidate.group)
Expand Down Expand Up @@ -675,7 +718,7 @@ func (p *ResponsesCompactionPlan) TrimForContextLimit() int {
message := item.Message
message.Images = nil
metadata, err := json.Marshal(compactionTranscriptItemWire{
Ref: item.Ref, Type: item.Type, Message: message, ImageCount: len(item.Message.Images),
Ref: item.Ref, Type: item.Type, Message: message, StandaloneName: item.StandaloneName, ImageCount: len(item.Message.Images),
})
if err != nil {
return 0
Expand Down Expand Up @@ -792,7 +835,7 @@ func (p *ResponsesCompactionPlan) summaryTranscriptMessages() ([]any, error) {
images := message.Images
message.Images = nil
metadata, err := json.Marshal(compactionTranscriptItemWire{
Ref: item.Ref, Type: item.Type, Message: message, ImageCount: len(images),
Ref: item.Ref, Type: item.Type, Message: message, StandaloneName: item.StandaloneName, ImageCount: len(images),
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -869,13 +912,21 @@ func (p *ResponsesCompactionPlan) Complete(body []byte) (ResponsesCompactionResu
}

retained := make([]api.Message, 0, len(selected))
var standaloneNames map[int]compactionFunctionName
for _, item := range p.items {
if _, ok := selected[item.Ref]; ok {
if item.StandaloneName != nil {
if standaloneNames == nil {
standaloneNames = make(map[int]compactionFunctionName)
}
standaloneNames[len(retained)] = *item.StandaloneName
}
retained = append(retained, item.Message)
}
}
payload := OllamaCompactionPayload{
Type: OllamaCompactionPayloadType, Version: OllamaCompactionPayloadVersion, Summary: selection.Summary, Retained: retained,
StandaloneNames: standaloneNames,
}
if p.omittedItems > 0 {
payload.Summary = fmt.Sprintf(compactionOmissionNotice, p.omittedItems) + "\n\n" + payload.Summary
Expand Down
Loading
Loading