mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-07-20 23:57:11 -04:00
[PR #247] [MERGED] Runtime provider switching #267
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/vxcontrol/pentagi/pull/247
Author: @asdek
Created: 4/8/2026
Status: ✅ Merged
Merged: 4/8/2026
Merged by: @asdek
Base:
main← Head:feature/switch_provider📝 Commits (1)
7c25f35feat: enable runtime provider switching📊 Changes
52 files changed (+594 additions, -196 deletions)
View changed files
📝
backend/cmd/ctester/main.go(+10 -10)📝
backend/pkg/controller/context.go(+3 -2)📝
backend/pkg/controller/flow.go(+65 -3)📝
backend/pkg/controller/subtask.go(+4 -0)📝
backend/pkg/database/flows.sql.go(+43 -0)📝
backend/pkg/database/msgchains.sql.go(+11 -3)📝
backend/pkg/database/querier.go(+1 -0)📝
backend/pkg/graph/generated.go(+31 -4)📝
backend/pkg/graph/schema.graphqls(+1 -1)📝
backend/pkg/graph/schema.resolvers.go(+24 -4)📝
backend/pkg/providers/anthropic/anthropic.go(+11 -1)📝
backend/pkg/providers/anthropic/anthropic_test.go(+2 -2)📝
backend/pkg/providers/assistant.go(+17 -4)📝
backend/pkg/providers/bedrock/bedrock.go(+11 -1)📝
backend/pkg/providers/bedrock/bedrock_test.go(+14 -14)📝
backend/pkg/providers/custom/custom.go(+11 -1)📝
backend/pkg/providers/custom/custom_test.go(+3 -3)📝
backend/pkg/providers/custom/example_test.go(+3 -2)📝
backend/pkg/providers/deepseek/deepseek.go(+11 -1)📝
backend/pkg/providers/deepseek/deepseek_test.go(+7 -7)...and 32 more files
📄 Description
Description of the Change
Problem
Two related but distinct problems affecting flow execution reliability and flexibility:
Message Chain Corruption on Server Restart:
LoadSubtaskWorkerwould resume execution using this corrupted chain directly viaPerformAgentChainwithout any consistency check"tool_call_ids did not have response messages", Anthropic reported"tool_use ids were found without tool_result blocks"No Runtime Provider Switching:
{f}:{r:1:d}format likecoder:14, Anthropic usestoolu_{r:24:b}) and reasoning content signatures that would be rejected by a different providerSolution
Conditional Chain Normalization in
processChain:The core fix is a single, well-placed normalization step in
processChain— the shared method that handles all message chain database operations. Before saving an updated chain back to the database, the method now comparesmsgChain.ModelProviderandmsgChain.Model(stored in the database record) against the currentflowProvider's active provider and model.NormalizeToolCallIDsconverts all tool call IDs to the new provider's format,ClearReasoningremoves provider-specific signatures, and if the last message is not a Human message, a continuation message ("Continue from where we left off") is appended to ensure the new provider receives a clean conversation boundaryThis approach means chain normalization happens exactly once — the next time a user adds input and
processChainis called — rather than unconditionally on everyPerformAgentChaincall, which would destroy reasoning cache.Pre-execution Chain Consistency:
EnsureChainConsistencyis called at the start ofsubtaskWorker.Run, beforePerformAgentChain. This usesNewChainAST(force=true)to add fallback tool responses for any pending tool calls left over from an interrupted run. Combined with the normalization inprocessChain, this covers the restart scenario: the chain is fixed when the user resumes, not when the backend blindly retries.Runtime Provider Switch via GraphQL:
The
putUserInputmutation now accepts an optionalmodelProviderparameter. When provided,switchProvideris called before the input is enqueued.switchProvidercalls the existingSetProvidermethod on the currentflowProvider— which atomically updates both the underlying provider instance and thetcIDTemplateunder a mutex — and then persists the change to theflowstable via a newUpdateFlowProviderSQL query. The executor and containers are reused entirely; no restart or reinitialization is needed.UpdateMsgChain with Provider Metadata:
The
UpdateMsgChainSQL query is extended to updatemodelandmodel_provideralongside the chain content. This ensures the stored metadata always reflects the provider that last wrote the chain, enabling accurate change detection on the nextprocessChaincall.Frontend Provider Selection:
The automation message form now allows provider selection when the flow is in
waitingstatus. The provider button is enabled only inwaitingstate and disabled in all other states (running,created,finished,failed). The selected provider name is passed to theputUserInputmutation.Closes #
Type of Change
Areas Affected
Testing and Verification
Test Configuration
Test Steps
waitingstate with an error rather than looping on API 400 errorsresumein the message input, verify the subtask continues without API errors and completes successfullywaitingstate, select a different provider (e.g. Anthropic) in the provider dropdown, submit a message — verify the flow continues with the new provider and no tool call ID format errors occurrunningorcreated, and enabled when flow iswaitingask_user, verify no unnecessary chain rewriting in logs (absence of"provider or model changed"log line)Test Results
resume— previously both looped with 400 errors until manual DB interventionadvice:2(Kimi format) correctly converted totoolu_...(Anthropic format) on provider switchrunning, unlocked duringwaitingSecurity Considerations
No Security Impact:
putUserInputSetProvidercall operates entirely within the in-processflowProviderstruct, no new network surfaces introducedUpdateFlowProviderupdates onlymodel_provider_name,model_provider_type,tool_call_id_template, andmodelfor the authenticated user's flowPerformance Impact
Improvements:
EnsureChainConsistencybeforePerformAgentChainprevents wasted API retries on a chain that would have been rejected anywayOverhead:
GetMsgChaincomparison perprocessChaincall (already fetched, no extra DB query)NormalizeToolCallIDs+ClearReasoning+AppendHumanMessagerun only on provider change — O(n) over chain messages, typically <1msDocumentation Updates
putUserInputgains optionalmodelProvider: StringparameterDeployment Notes
Database Migration:
UpdateMsgChainSQL extended withmodelandmodel_providerparameters (sqlc-generated, backward compatible — existing rows retain their current values)UpdateFlowProviderSQL query added toflowstable (no schema change)Environment Variables:
Deployment Steps:
docker compose builddocker compose up -dCompatibility:
modelProvideris optional; all existing clients continue to workChecklist
Code Quality
go fmtandgo vet(for Go code)npm run lint(for TypeScript/JavaScript code)Security
Compatibility
Documentation
Additional Notes
Key Design Decisions
Why normalize in
processChainrather thanPerformAgentChain:PerformAgentChainruns on every subtask iteration, including iterations where the provider has not changed. Normalizing there would clear reasoning signatures unconditionally, eliminating any KV cache benefit from the LLM backend.processChainis only called when new user input is being stored — exactly the moment a new conversation boundary exists and normalization is safe.Why add a continuation Human message:
When a provider is switched and the last message in the chain is not a Human message (e.g., the previous turn ended with a tool call whose response was just added), the new provider would receive a chain ending with a Tool message. Some providers reject this or behave unpredictably. Appending
"Continue from where we left off"creates a clean conversation turn that any provider can handle correctly without changing the semantic content of the task.Why reuse the executor on provider switch:
The Docker containers holding pentest tool environments are expensive to create and are unrelated to the LLM provider.
SetProviderreplaces only the LLM client andtcIDTemplateinside the existingflowProviderstruct under a mutex, leaving the executor, image reference, and all log workers intact.Why the restart scenario still requires user input:
This is intentional. The backend cannot safely resume execution automatically after an unknown interruption — the user should confirm the intent to continue. Once the user submits any input,
processChainfires, fixes the chain, and execution resumes. This matches the existing graceful degradation model.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.