feat: add configurable reasoning effort to Coder agents (#26974)

This commit is contained in:
Danielle Maywood
2026-07-09 23:35:12 +01:00
committed by GitHub
parent 5fed583a46
commit d66e4d794f
72 changed files with 4389 additions and 1275 deletions
@@ -0,0 +1,61 @@
DROP VIEW IF EXISTS chats_expanded;
-- The up migration left the legacy per-provider effort keys in
-- place, so removing the reasoning_effort key restores the previous
-- state exactly.
UPDATE chat_model_configs
SET options = options - 'reasoning_effort'
WHERE options ? 'reasoning_effort';
ALTER TABLE chats DROP COLUMN last_reasoning_effort;
ALTER TABLE chat_messages DROP COLUMN reasoning_effort;
ALTER TABLE chat_queued_messages DROP COLUMN reasoning_effort;
DROP TYPE chat_reasoning_effort;
CREATE VIEW chats_expanded AS
SELECT c.id,
c.owner_id,
c.workspace_id,
c.title,
c.status,
c.worker_id,
c.started_at,
c.heartbeat_at,
c.created_at,
c.updated_at,
c.parent_chat_id,
c.root_chat_id,
c.last_model_config_id,
c.archived,
c.last_error,
c.mode,
c.mcp_server_ids,
c.labels,
c.build_id,
c.agent_id,
c.pin_order,
c.last_read_message_id,
c.dynamic_tools,
c.organization_id,
c.plan_mode,
c.client_type,
c.last_turn_summary,
c.snapshot_version,
c.history_version,
c.queue_version,
c.generation_attempt,
c.retry_state,
c.retry_state_version,
c.runner_id,
c.requires_action_deadline_at,
COALESCE(root.user_acl, c.user_acl) AS user_acl,
COALESCE(root.group_acl, c.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
c.context_aggregate_hash,
c.context_dirty_since,
c.context_dirty_resources,
c.context_error
FROM ((chats c
LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id))))
JOIN visible_users owner ON ((owner.id = c.owner_id)));
@@ -0,0 +1,88 @@
-- Per-turn reasoning effort. The chats_expanded view must be dropped
-- and recreated so the new chats column can appear in its column list.
DROP VIEW IF EXISTS chats_expanded;
CREATE TYPE chat_reasoning_effort AS ENUM ('none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max');
ALTER TABLE chats ADD COLUMN last_reasoning_effort chat_reasoning_effort;
ALTER TABLE chat_messages ADD COLUMN reasoning_effort chat_reasoning_effort;
ALTER TABLE chat_queued_messages ADD COLUMN reasoning_effort chat_reasoning_effort;
COMMENT ON COLUMN chats.last_reasoning_effort IS 'Stores the most recent message effort once per-turn selection is wired.';
COMMENT ON COLUMN chat_messages.reasoning_effort IS 'Stores the selected effort for the turn triggered by this message.';
COMMENT ON COLUMN chat_queued_messages.reasoning_effort IS 'Stores the selected effort until the queued row is promoted.';
CREATE VIEW chats_expanded AS
SELECT c.id,
c.owner_id,
c.workspace_id,
c.title,
c.status,
c.worker_id,
c.started_at,
c.heartbeat_at,
c.created_at,
c.updated_at,
c.parent_chat_id,
c.root_chat_id,
c.last_model_config_id,
c.last_reasoning_effort,
c.archived,
c.last_error,
c.mode,
c.mcp_server_ids,
c.labels,
c.build_id,
c.agent_id,
c.pin_order,
c.last_read_message_id,
c.dynamic_tools,
c.organization_id,
c.plan_mode,
c.client_type,
c.last_turn_summary,
c.snapshot_version,
c.history_version,
c.queue_version,
c.generation_attempt,
c.retry_state,
c.retry_state_version,
c.runner_id,
c.requires_action_deadline_at,
COALESCE(root.user_acl, c.user_acl) AS user_acl,
COALESCE(root.group_acl, c.group_acl) AS group_acl,
owner.username AS owner_username,
owner.name AS owner_name,
c.context_aggregate_hash,
c.context_dirty_since,
c.context_dirty_resources,
c.context_error
FROM ((chats c
LEFT JOIN chats root ON ((root.id = COALESCE(c.root_chat_id, c.parent_chat_id))))
JOIN visible_users owner ON ((owner.id = c.owner_id)));
-- Copy legacy per-provider effort values to top-level
-- reasoning_effort. Preserve legacy keys so the down migration can
-- restore the original options shape.
UPDATE chat_model_configs
SET options = options || jsonb_build_object(
'reasoning_effort',
jsonb_build_object('default', legacy.effort, 'max', legacy.effort)
)
FROM (
SELECT
id,
COALESCE(
NULLIF(lower(trim(options #>> '{provider_options,openai,reasoning_effort}')), ''),
NULLIF(lower(trim(options #>> '{provider_options,azure,reasoning_effort}')), ''),
NULLIF(lower(trim(options #>> '{provider_options,anthropic,effort}')), ''),
NULLIF(lower(trim(options #>> '{provider_options,bedrock,effort}')), ''),
NULLIF(lower(trim(options #>> '{provider_options,openaicompat,reasoning_effort}')), ''),
NULLIF(lower(trim(options #>> '{provider_options,openrouter,reasoning,effort}')), ''),
NULLIF(lower(trim(options #>> '{provider_options,vercel,reasoning,effort}')), '')
) AS effort
FROM chat_model_configs
) legacy
WHERE chat_model_configs.id = legacy.id
AND legacy.effort IS NOT NULL
AND legacy.effort IN ('none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max');
@@ -1582,6 +1582,80 @@ func applyMigrationsInTxn(ctx context.Context, t *testing.T, sqlDB *sql.DB, from
require.NoError(t, tx.Commit())
}
func TestMigration000542ChatReasoningEffortBackfill(t *testing.T) {
t.Parallel()
const priorMigrationVersion = 539
sqlDB := testSQLDB(t)
next, err := migrations.Stepper(sqlDB)
require.NoError(t, err)
for {
version, more, err := next()
require.NoError(t, err)
if !more || version == priorMigrationVersion {
break
}
}
ctx := testutil.Context(t, testutil.WaitSuperLong)
now := time.Now().UTC().Truncate(time.Microsecond)
tx, err := sqlDB.BeginTx(ctx, nil)
require.NoError(t, err)
defer tx.Rollback()
azureID := uuid.New()
bedrockID := uuid.New()
emptyID := uuid.New()
invalidID := uuid.New()
_, err = tx.ExecContext(ctx, `
INSERT INTO ai_providers (id, type, name, enabled, base_url, created_at, updated_at)
VALUES
($1, 'azure', 'test-azure-reasoning', TRUE, '', $3, $3),
($2, 'bedrock', 'test-bedrock-reasoning', TRUE, '', $3, $3)
`, azureID, bedrockID, now)
require.NoError(t, err)
_, err = tx.ExecContext(ctx, `
INSERT INTO chat_model_configs (id, ai_provider_id, model, display_name, enabled, context_limit, compression_threshold, options, created_at, updated_at)
VALUES
($3, $1, 'gpt-5.1-azure', 'Azure GPT-5.1', TRUE, 200000, 70, '{"provider_options": {"azure": {"reasoning_effort": " LOW "}}}', $5, $5),
($4, $2, 'anthropic.claude-opus-4-6', 'Bedrock Claude Opus', TRUE, 200000, 70, '{"provider_options": {"bedrock": {"effort": "minimal"}}}', $5, $5),
(gen_random_uuid(), $1, 'gpt-5.1-empty-effort', 'Azure Empty Effort', TRUE, 200000, 70, '{"provider_options": {"azure": {"reasoning_effort": ""}}}', $5, $5),
(gen_random_uuid(), $2, 'anthropic.invalid-effort', 'Bedrock Invalid Effort', TRUE, 200000, 70, '{"provider_options": {"bedrock": {"effort": "extreme"}}}', $5, $5)
`, azureID, bedrockID, emptyID, invalidID, now)
require.NoError(t, err)
require.NoError(t, tx.Commit())
migrationSQL, err := os.ReadFile("000542_chat_reasoning_effort.up.sql")
require.NoError(t, err)
_, err = sqlDB.ExecContext(ctx, string(migrationSQL))
require.NoError(t, err)
rows, err := sqlDB.QueryContext(ctx, `
SELECT ap.type::text, cmc.model, cmc.options->'reasoning_effort'->>'default'
FROM chat_model_configs cmc
JOIN ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE cmc.ai_provider_id IN ($1, $2)
ORDER BY cmc.model
`, azureID, bedrockID)
require.NoError(t, err)
defer rows.Close()
got := map[string]sql.NullString{}
for rows.Next() {
var provider, model string
var effort sql.NullString
require.NoError(t, rows.Scan(&provider, &model, &effort))
got[provider+":"+model] = effort
}
require.NoError(t, rows.Err())
require.Equal(t, sql.NullString{String: "low", Valid: true}, got["azure:gpt-5.1-azure"])
require.Equal(t, sql.NullString{}, got["azure:gpt-5.1-empty-effort"])
require.Equal(t, sql.NullString{String: "minimal", Valid: true}, got["bedrock:anthropic.claude-opus-4-6"])
require.Equal(t, sql.NullString{}, got["bedrock:anthropic.invalid-effort"])
}
func TestMigration000498SoftDeleteStaleWorkspaceAgents(t *testing.T) {
t.Parallel()
@@ -7,16 +7,47 @@ INSERT INTO chat_providers (
enabled,
created_at,
updated_at
) VALUES (
'0a8b2f84-b5a8-4c44-8c9f-e58c44a534a7',
'openai',
'OpenAI',
'',
NULL,
TRUE,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
);
) VALUES
(
'0a8b2f84-b5a8-4c44-8c9f-e58c44a534a7',
'openai',
'OpenAI',
'',
NULL,
TRUE,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6e02',
'anthropic',
'Anthropic (Reasoning Effort Fixture)',
'',
NULL,
TRUE,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6e03',
'azure',
'Azure OpenAI (Reasoning Effort Fixture)',
'',
NULL,
TRUE,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6e04',
'bedrock',
'Bedrock (Reasoning Effort Fixture)',
'',
NULL,
TRUE,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
);
INSERT INTO chat_model_configs (
id,
@@ -26,19 +57,94 @@ INSERT INTO chat_model_configs (
enabled,
context_limit,
compression_threshold,
options,
created_at,
updated_at
) VALUES (
'9af5f8d5-6a57-4505-8a69-3d6c787b95fd',
'openai',
'gpt-5.2',
'GPT 5.2',
TRUE,
200000,
70,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
);
) VALUES
(
'9af5f8d5-6a57-4505-8a69-3d6c787b95fd',
'openai',
'gpt-5.2',
'GPT 5.2',
TRUE,
200000,
70,
'{}'::jsonb,
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6f01',
'openai',
'gpt-5.1',
'GPT-5.1 (Legacy Effort)',
TRUE,
200000,
70,
'{"provider_options": {"openai": {"reasoning_effort": " HIGH ", "reasoning_summary": "auto"}}}',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6f02',
'anthropic',
'claude-opus-4-6',
'Claude Opus (Legacy Effort)',
TRUE,
200000,
70,
'{"provider_options": {"anthropic": {"effort": "max", "send_reasoning": true}}}',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6f05',
'azure',
'gpt-5.1-azure',
'Azure GPT-5.1 (Legacy Effort)',
TRUE,
200000,
70,
'{"provider_options": {"azure": {"reasoning_effort": "low"}}}',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6f06',
'bedrock',
'anthropic.claude-opus-4-6',
'Bedrock Claude Opus (Legacy Effort)',
TRUE,
200000,
70,
'{"provider_options": {"bedrock": {"effort": "minimal"}}}',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6f03',
'openai',
'gpt-5.1-empty-effort',
'GPT-5.1 (Empty Legacy Effort)',
TRUE,
200000,
70,
'{"provider_options": {"openai": {"reasoning_effort": ""}}}',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
),
(
'4f0a9c2e-1d3b-4a5c-8e7f-6a9b8c7d6f04',
'openai',
'gpt-5.1-invalid-effort',
'GPT-5.1 (Invalid Legacy Effort)',
TRUE,
200000,
70,
'{"provider_options": {"openai": {"reasoning_effort": "extreme"}}}',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
);
INSERT INTO chats (
id,