Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -584,15 +584,17 @@ struct HatchingStepView: View {
private static let dockerReadySentinel = "Docker containers are up and running"

/// Build the --config key=value pairs for the onboarding selections.
/// Build config overrides to pass as --config flags during hatch.
///
/// Most config default values are determined by the daemon process and may
/// depend on whether the assistant is hatched on the Vellum Platform or not.
/// Delegates to `onboardingHatchConfigOverlay` — a pure top-level helper
/// that encodes the two distinct shapes (managed inference vs. BYOK)
/// based on whether the user signed in and skipped the provider API key
/// entry step.
private func buildOnboardingConfigValues() -> [String: String] {
let provider = state.selectedProvider.isEmpty
? LLMProviderRegistry.defaultProvider.id
: state.selectedProvider
return ["llm.default.provider": provider]
return onboardingHatchConfigOverlay(
skippedAPIKeyEntry: state.skippedAPIKeyEntry,
selectedProvider: state.selectedProvider,
defaultProvider: LLMProviderRegistry.defaultProvider.id
)
}

private func startRemoteHatch() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

/// Build the `--config key=value` overlay the macOS app sends to the CLI
/// (`vellum hatch`) during the onboarding hatch step.
///
/// The CLI persists this overlay to a temp JSON file and passes the path to
/// the daemon via `VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH`. At daemon startup,
/// `mergeDefaultWorkspaceConfig` merges it into the workspace config on disk
/// and `seedInferenceProfiles` consumes it via `preserveActiveProfile`.
///
/// Two distinct shapes:
///
/// 1. **Managed-inference path** (`skippedAPIKeyEntry == true`).
/// The user signed in with their Vellum account and chose a non-cloud
/// hosting mode (Local / AWS / GCP / SSH). They did *not* supply a
/// provider API key — chat traffic will route through the managed
/// Anthropic proxy via the platform-injected `assistant_api_key`. Emit
/// `llm.activeProfile = "balanced"` so the seeder preserves the managed
/// profile (which points at `anthropic-managed`) and skips
/// user-profile/personal-connection materialization. Without this the
/// seeder would otherwise pick `custom-balanced` and the daemon would
/// fail to send messages on a missing `credential/anthropic/api_key`.
///
/// 2. **BYOK path** (`skippedAPIKeyEntry == false`).
/// The user entered their own provider API key. Emit
/// `llm.default.provider = <provider>` so the seeder materializes
/// `<provider>-personal` and the matching `custom-*` profiles. The
/// seeder picks `custom-balanced` as the active profile in this case.
/// Preserves historical "default to anthropic when empty" behavior.
func onboardingHatchConfigOverlay(
skippedAPIKeyEntry: Bool,
selectedProvider: String,
defaultProvider: String
) -> [String: String] {
if skippedAPIKeyEntry {
return ["llm.activeProfile": "balanced"]
}
let provider = selectedProvider.isEmpty ? defaultProvider : selectedProvider
return ["llm.default.provider": provider]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import XCTest
@testable import VellumAssistantLib

final class OnboardingHatchConfigOverlayTests: XCTestCase {

// MARK: - Managed-inference path (skippedAPIKeyEntry == true)

func testManagedInferenceEmitsBalancedActiveProfile() {
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: true,
selectedProvider: "anthropic",
defaultProvider: "anthropic"
)

XCTAssertEqual(overlay, ["llm.activeProfile": "balanced"])
}

func testManagedInferenceIgnoresSelectedProvider() {
// Even if the UI default provider differs, managed inference always
// points at the daemon-seeded `balanced` profile (managed Anthropic).
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: true,
selectedProvider: "openai",
defaultProvider: "anthropic"
)

XCTAssertEqual(overlay, ["llm.activeProfile": "balanced"])
}

func testManagedInferenceDoesNotEmitDefaultProvider() {
// Crucial invariant: no `llm.default.provider` so the seeder does NOT
// materialize an `anthropic-personal` connection during hatch seeding.
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: true,
selectedProvider: "anthropic",
defaultProvider: "anthropic"
)

XCTAssertNil(overlay["llm.default.provider"])
}

// MARK: - BYOK path (skippedAPIKeyEntry == false)

func testBYOKEmitsSelectedProviderAsDefault() {
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: false,
selectedProvider: "openai",
defaultProvider: "anthropic"
)

XCTAssertEqual(overlay, ["llm.default.provider": "openai"])
}

func testBYOKWithAnthropicProvider() {
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: false,
selectedProvider: "anthropic",
defaultProvider: "anthropic"
)

XCTAssertEqual(overlay, ["llm.default.provider": "anthropic"])
}

func testBYOKFallsBackToDefaultProviderWhenSelectedIsEmpty() {
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: false,
selectedProvider: "",
defaultProvider: "anthropic"
)

XCTAssertEqual(overlay, ["llm.default.provider": "anthropic"])
}

func testBYOKUsesInjectedDefaultProviderWhenSelectedIsEmpty() {
// The fallback isn't hardcoded — it tracks whatever the registry says.
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: false,
selectedProvider: "",
defaultProvider: "openai"
)

XCTAssertEqual(overlay, ["llm.default.provider": "openai"])
}

func testBYOKDoesNotEmitActiveProfile() {
// The BYOK path lets the daemon seeder pick the active profile based
// on whether a user connection is materialized — emitting
// `llm.activeProfile` here would short-circuit that resolution.
let overlay = onboardingHatchConfigOverlay(
skippedAPIKeyEntry: false,
selectedProvider: "anthropic",
defaultProvider: "anthropic"
)

XCTAssertNil(overlay["llm.activeProfile"])
}
}