fix(init): wire Custom provider through OpenAI-compatible runtime path (closes #83)#93
Open
initializ-mk wants to merge 1 commit into
Open
fix(init): wire Custom provider through OpenAI-compatible runtime path (closes #83)#93initializ-mk wants to merge 1 commit into
initializ-mk wants to merge 1 commit into
Conversation
…ath (closes #83) The wizard's Custom provider option is documented as "point Forge at any OpenAI-compatible endpoint" (OpenRouter, litellm, vLLM, self-hosted Kimi/ Llama, etc.). It collected the right inputs but wrote artifacts the runtime could not consume: 1. forge.yaml got `provider: custom`, which the LLM client factory (forge-core/llm/providers/factory.go) rejects with "unknown LLM provider". Runner falls back to StubExecutor; every task fails with 'agent execution not configured for framework "forge"'. 2. .env got MODEL_BASE_URL / MODEL_API_KEY, which forge-core/runtime/ config.go's resolveAPIKey() never reads (only per-provider names like OPENAI_BASE_URL / OPENAI_API_KEY are consulted). 3. Even when users worked around (1) and (2) by hand-editing forge.yaml to `provider: openai` + writing OPENAI_BASE_URL, stored ChatGPT OAuth credentials silently won over the explicit base URL — the runner's createProviderClient overrides cfg.BaseURL with chatgpt.com/backend-api/codex. ChatGPT then returns 400 rejecting the operator's model, with no signal that the endpoint was hijacked. Fix at three boundaries: - forge-cli/cmd/init.go: new normalizeCustomProvider() called at the top of scaffold() (single entry point for both TUI and Web UI). Rewrites ModelProvider=custom -> openai and migrates MODEL_BASE_URL / MODEL_API_KEY -> OPENAI_BASE_URL / OPENAI_API_KEY. Handles all three shapes that may arrive: legacy MODEL_* env vars, newer Web UI direct OPENAI_* env vars, and the non-interactive --api-key flag path. buildEnvVars's "openai" case now emits OPENAI_BASE_URL when set; the dead "custom" case is removed. - forge-cli/runtime/runner.go + forge-cli/cmd/ui.go: OAuth-precedence guardrail. When provider="openai" + cfg.BaseURL != "" + cfg.APIKey == "", refuse rather than fall through to OAuth. Error names the configured base URL, the missing env var (OPENAI_API_KEY), and the override (chatgpt.com/backend-api/codex) that would otherwise occur. Anthropic and other providers unaffected. - forge-ui/handlers_create.go + types.go + static/app.js: wizard metadata advertises BaseURLEnv="OPENAI_BASE_URL" (not MODEL_BASE_URL); the React form binds to OPENAI_BASE_URL directly. POSTs whose ModelProvider="custom" still work — they hit the same scaffold() entry point and get the same normalization. Regression tests: forge-cli/cmd/init_custom_provider_test.go (6 tests): TestNormalizeCustomProvider_RewritesLegacyEnvVars TestNormalizeCustomProvider_NoOpForOtherProviders TestNormalizeCustomProvider_PreExistingOpenAIVarsPreserved TestNormalizeCustomProvider_APIKeyFallsBackToOptsField TestScaffold_CustomProviderProducesOpenAIShape TestScaffold_CustomProviderWebUIShape forge-cli/runtime/runner_oauth_guardrail_test.go (4 tests): TestCreateProviderClient_BaseURLSetWithoutAPIKey_RefusesOAuth TestCreateProviderClient_BaseURLSetWithAPIKey_BypassesOAuth TestCreateProviderClient_NoBaseURL_AllowsOAuthPath TestCreateProviderClient_AnthropicWithBaseURL_Unaffected go test -race ./forge-cli/cmd/... ./forge-cli/runtime/... ./forge-ui/... golangci-lint and gofmt clean across all three modules. Migration note in CHANGELOG for users with checked-in `provider: custom` forge.yaml files (manual rename to provider: openai + .env key rename).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #83.
The wizard's Custom provider option is documented as "point Forge at any OpenAI-compatible endpoint" (OpenRouter, litellm, vLLM, self-hosted Kimi/Llama, etc.). It collected the right inputs but wrote artifacts the runtime could not consume:
forge.yamlgotprovider: custom, which the LLM client factory rejects (forge-core/llm/providers/factory.gohas nocase "custom":). The runner falls back toStubExecutorand every task fails withagent execution not configured for framework "forge"..envgotMODEL_BASE_URL/MODEL_API_KEY, whichResolveModelConfignever reads — only per-provider names (OPENAI_BASE_URL, etc.) are consulted.OPENAI_BASE_URL—createProviderClientreplacescfg.BaseURLwithchatgpt.com/backend-api/codex. ChatGPT then 400s on the operator's model, with no signal that the endpoint was hijacked.Fix
Wire 1+2 —
forge-cli/cmd/init.goNew
normalizeCustomProvider()called at the top ofscaffold(). Single entry point covers both the TUI wizard and the Web UI POST. RewritesModelProvider=custom→openaiand migratesMODEL_BASE_URL/MODEL_API_KEY→OPENAI_BASE_URL/OPENAI_API_KEY.Handles all three shapes that may arrive:
MODEL_*env vars (current TUI wizard, older Web UI).OPENAI_*env vars written directly (post-fix Web UI).--api-keyflag whereopts.APIKeyis set but no env var is.buildEnvVarsextended to emitOPENAI_BASE_URLwhen set; deadcase "custom":removed.OAuth-precedence guardrail —
forge-cli/runtime/runner.go+forge-cli/cmd/ui.goWhen
provider == "openai"ANDcfg.BaseURL != ""ANDcfg.APIKey == "", refuse rather than fall through to OAuth. Error explicitly names:OPENAI_API_KEY)chatgpt.com/backend-api/codex) that would otherwise occur silentlyApplied to both the agent runtime (
runner.go) and the skill builder chat handler (cmd/ui.go). Anthropic / Gemini / Ollama paths unaffected.Web UI —
forge-ui/handlers_create.go+types.go+static/app.jsWizard metadata advertises
BaseURLEnv: \"OPENAI_BASE_URL\"(was\"MODEL_BASE_URL\"); the React form binds toOPENAI_BASE_URLdirectly. POSTs whoseModelProvider == \"custom\"still work — they hit the samescaffold()entry point and get normalized identically.Regression tests
10 new tests, all
-racegreen:forge-cli/cmd/init_custom_provider_test.go(6 tests)TestNormalizeCustomProvider_RewritesLegacyEnvVars— oldMODEL_*shape →OPENAI_*TestNormalizeCustomProvider_NoOpForOtherProviders— table across openai/anthropic/gemini/ollamaTestNormalizeCustomProvider_PreExistingOpenAIVarsPreserved— newer Web UI shape isn't clobberedTestNormalizeCustomProvider_APIKeyFallsBackToOptsField— non-interactive--api-keypathTestScaffold_CustomProviderProducesOpenAIShape— end-to-end; pinsforge.yaml+.envcontent; asserts noMODEL_*leakTestScaffold_CustomProviderWebUIShape— parity with Web UI POST shapeforge-cli/runtime/runner_oauth_guardrail_test.go(4 tests)TestCreateProviderClient_BaseURLSetWithoutAPIKey_RefusesOAuth— pins the guardrail + verifies the error message mentionsOPENAI_BASE_URL, the configured URL, andOPENAI_API_KEYTestCreateProviderClient_BaseURLSetWithAPIKey_BypassesOAuth— happy path still worksTestCreateProviderClient_NoBaseURL_AllowsOAuthPath— existing openai.com OAuth use unaffectedTestCreateProviderClient_AnthropicWithBaseURL_Unaffected— guardrail scoped to openaiGates
go test -race -count=1 ./forge-cli/cmd/... ./forge-cli/runtime/... ./forge-ui/...— greengolangci-lint run— 0 issues across forge-cli, forge-ui, forge-coregofmt -l forge-cli/ forge-ui/ forge-core/— cleanMigration
CHANGELOG "Unreleased" section calls out the rename for users with checked-in
provider: customconfigs:Test plan
go test -race -count=1 ./forge-cli/cmd/... ./forge-cli/runtime/... ./forge-ui/...greengolangci-lint runclean across the three modulesgofmt -lcleanprovider: openai+OPENAI_BASE_URL/OPENAI_API_KEY(pinned byTestScaffold_CustomProviderProducesOpenAIShape)TestScaffold_CustomProviderWebUIShape)OPENAI_BASE_URLwithoutOPENAI_API_KEY→ clear error, no silent ChatGPT redirect (pinned byTestCreateProviderClient_BaseURLSetWithoutAPIKey_RefusesOAuth)eurlconfig against OpenRouter / Kimi)