fix: use previous_response_id chaining for OpenAI store=true follow-ups (#23450)

OpenAI Responses follow-up turns were replaying full assistant/tool
history even when `store=true`, which breaks after reasoning +
provider-executed `web_search` output.

This change persists the OpenAI response ID on assistant messages, then
in `coderd/x/chatd` switches `store=true` follow-ups to
`previous_response_id` chaining with a system + new-user-only prompt.
`store=false` and missing-ID cases still fall back to manual replay.

It also updates the fake OpenAI server and integration coverage for the
chaining contract, and carries the rebased path move to `coderd/x/chatd`
plus the migration renumber needed after rebasing onto `main`.
This commit is contained in:
Michael Suchacz
2026-03-24 14:57:40 +01:00
committed by GitHub
parent b9f0c479ac
commit 02356c61f6
12 changed files with 935 additions and 93 deletions
+2 -1
View File
@@ -1294,7 +1294,8 @@ CREATE TABLE chat_messages (
content_version smallint NOT NULL,
total_cost_micros bigint,
runtime_ms bigint,
deleted boolean DEFAULT false NOT NULL
deleted boolean DEFAULT false NOT NULL,
provider_response_id text
);
CREATE SEQUENCE chat_messages_id_seq
@@ -0,0 +1 @@
ALTER TABLE chat_messages DROP COLUMN provider_response_id;
@@ -0,0 +1 @@
ALTER TABLE chat_messages ADD COLUMN provider_response_id TEXT;
+1
View File
@@ -4229,6 +4229,7 @@ type ChatMessage struct {
TotalCostMicros sql.NullInt64 `db:"total_cost_micros" json:"total_cost_micros"`
RuntimeMs sql.NullInt64 `db:"runtime_ms" json:"runtime_ms"`
Deleted bool `db:"deleted" json:"deleted"`
ProviderResponseID sql.NullString `db:"provider_response_id" json:"provider_response_id"`
}
type ChatModelConfig struct {
+20 -9
View File
@@ -4623,7 +4623,7 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [
const getChatMessageByID = `-- name: GetChatMessageByID :one
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
FROM
chat_messages
WHERE
@@ -4655,13 +4655,14 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
)
return i, err
}
const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
FROM
chat_messages
WHERE
@@ -4708,6 +4709,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
); err != nil {
return nil, err
}
@@ -4724,7 +4726,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes
const getChatMessagesByChatIDDescPaginated = `-- name: GetChatMessagesByChatIDDescPaginated :many
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
FROM
chat_messages
WHERE
@@ -4777,6 +4779,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, a
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
); err != nil {
return nil, err
}
@@ -4809,7 +4812,7 @@ WITH latest_compressed_summary AS (
1
)
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
FROM
chat_messages
WHERE
@@ -4880,6 +4883,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
); err != nil {
return nil, err
}
@@ -5085,7 +5089,7 @@ func (q *sqlQuerier) GetChats(ctx context.Context, arg GetChatsParams) ([]Chat,
const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one
SELECT
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
FROM
chat_messages
WHERE
@@ -5127,6 +5131,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
)
return i, err
}
@@ -5339,7 +5344,8 @@ INSERT INTO chat_messages (
context_limit,
compressed,
total_cost_micros,
runtime_ms
runtime_ms,
provider_response_id
)
SELECT
$1::uuid,
@@ -5358,9 +5364,10 @@ SELECT
NULLIF(UNNEST($14::bigint[]), 0),
UNNEST($15::boolean[]),
NULLIF(UNNEST($16::bigint[]), 0),
NULLIF(UNNEST($17::bigint[]), 0)
NULLIF(UNNEST($17::bigint[]), 0),
NULLIF(UNNEST($18::text[]), '')
RETURNING
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
`
type InsertChatMessagesParams struct {
@@ -5381,6 +5388,7 @@ type InsertChatMessagesParams struct {
Compressed []bool `db:"compressed" json:"compressed"`
TotalCostMicros []int64 `db:"total_cost_micros" json:"total_cost_micros"`
RuntimeMs []int64 `db:"runtime_ms" json:"runtime_ms"`
ProviderResponseID []string `db:"provider_response_id" json:"provider_response_id"`
}
func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error) {
@@ -5402,6 +5410,7 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa
pq.Array(arg.Compressed),
pq.Array(arg.TotalCostMicros),
pq.Array(arg.RuntimeMs),
pq.Array(arg.ProviderResponseID),
)
if err != nil {
return nil, err
@@ -5431,6 +5440,7 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
); err != nil {
return nil, err
}
@@ -5789,7 +5799,7 @@ SET
WHERE
id = $3::bigint
RETURNING
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted
id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id
`
type UpdateChatMessageByIDParams struct {
@@ -5822,6 +5832,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe
&i.TotalCostMicros,
&i.RuntimeMs,
&i.Deleted,
&i.ProviderResponseID,
)
return i, err
}
+4 -2
View File
@@ -241,7 +241,8 @@ INSERT INTO chat_messages (
context_limit,
compressed,
total_cost_micros,
runtime_ms
runtime_ms,
provider_response_id
)
SELECT
@chat_id::uuid,
@@ -260,7 +261,8 @@ SELECT
NULLIF(UNNEST(@context_limit::bigint[]), 0),
UNNEST(@compressed::boolean[]),
NULLIF(UNNEST(@total_cost_micros::bigint[]), 0),
NULLIF(UNNEST(@runtime_ms::bigint[]), 0)
NULLIF(UNNEST(@runtime_ms::bigint[]), 0),
NULLIF(UNNEST(@provider_response_id::text[]), '')
RETURNING
*;