diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index ccf1f5a91a..e5b5c0d00a 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -132,6 +132,7 @@ const buildCmd = Command.make( cwd: serverDir, stdout: config.verbose ? "inherit" : "ignore", stderr: "inherit", + shell: process.platform === "win32", })`bun tsdown`, ); @@ -225,6 +226,7 @@ const publishCmd = Command.make( cwd: serverDir, stdout: config.verbose ? "inherit" : "ignore", stderr: "inherit", + shell: process.platform === "win32", }), ); }), diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index d34791bc20..2a72d59027 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -6,6 +6,7 @@ import { type ProviderModelOptions, type ProviderKind, type ProviderServiceTier, + type ProviderStartOptions, type OrchestrationSession, ThreadId, type ProviderSession, @@ -144,6 +145,8 @@ const make = Effect.gen(function* () { ), ); + const threadProviderOptions = new Map(); + const appendProviderFailureActivity = (input: { readonly threadId: ThreadId; readonly kind: @@ -203,6 +206,7 @@ const make = Effect.gen(function* () { readonly model?: string; readonly modelOptions?: ProviderModelOptions; readonly serviceTier?: ProviderServiceTier | null; + readonly providerOptions?: ProviderStartOptions; }, ) { const readModel = yield* orchestrationEngine.getReadModel(); @@ -239,6 +243,7 @@ const make = Effect.gen(function* () { ...(desiredModel ? { model: desiredModel } : {}), ...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}), ...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}), + ...(options?.providerOptions !== undefined ? { providerOptions: options.providerOptions } : {}), ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), runtimeMode: desiredRuntimeMode, }); @@ -325,6 +330,7 @@ const make = Effect.gen(function* () { readonly model?: string; readonly serviceTier?: ProviderServiceTier | null; readonly modelOptions?: ProviderModelOptions; + readonly providerOptions?: ProviderStartOptions; readonly interactionMode?: "default" | "plan"; readonly createdAt: string; }) { @@ -332,11 +338,15 @@ const make = Effect.gen(function* () { if (!thread) { return; } + if (input.providerOptions !== undefined) { + threadProviderOptions.set(input.threadId, input.providerOptions); + } yield* ensureSessionForThread(input.threadId, input.createdAt, { ...(input.provider !== undefined ? { provider: input.provider } : {}), ...(input.model !== undefined ? { model: input.model } : {}), ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), ...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}), + ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), }); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; @@ -472,6 +482,7 @@ const make = Effect.gen(function* () { ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}), ...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}), + ...(event.payload.providerOptions !== undefined ? { providerOptions: event.payload.providerOptions } : {}), interactionMode: event.payload.interactionMode, createdAt: event.payload.createdAt, }); @@ -627,7 +638,10 @@ const make = Effect.gen(function* () { if (!thread?.session || thread.session.status === "stopped") { return; } - yield* ensureSessionForThread(event.payload.threadId, event.occurredAt); + const cachedProviderOptions = threadProviderOptions.get(event.payload.threadId); + yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, { + ...(cachedProviderOptions !== undefined ? { providerOptions: cachedProviderOptions } : {}), + }); return; } case "thread.turn-start-requested": diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index f036416429..1c1ca4dd79 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -303,6 +303,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ...(command.model !== undefined ? { model: command.model } : {}), ...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}), ...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}), + ...(command.providerOptions !== undefined ? { providerOptions: command.providerOptions } : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, runtimeMode: readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ?? diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts index 398a26fb7b..05a1de1495 100644 --- a/apps/server/src/provider/Layers/ProviderService.ts +++ b/apps/server/src/provider/Layers/ProviderService.ts @@ -86,15 +86,30 @@ function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "st } } -function toRuntimePayloadFromSession(session: ProviderSession): Record { +function toRuntimePayloadFromSession( + session: ProviderSession, + extra?: { readonly providerOptions?: unknown }, +): Record { return { cwd: session.cwd ?? null, model: session.model ?? null, activeTurnId: session.activeTurnId ?? null, lastError: session.lastError ?? null, + ...(extra?.providerOptions !== undefined ? { providerOptions: extra.providerOptions } : {}), }; } +function readPersistedProviderOptions( + runtimePayload: ProviderRuntimeBinding["runtimePayload"], +): Record | undefined { + if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) { + return undefined; + } + const raw = "providerOptions" in runtimePayload ? runtimePayload.providerOptions : undefined; + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + return raw as Record; +} + function readPersistedCwd( runtimePayload: ProviderRuntimeBinding["runtimePayload"], ): string | undefined { @@ -137,6 +152,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => const upsertSessionBinding = ( session: ProviderSession, threadId: ThreadId, + extra?: { readonly providerOptions?: unknown }, ) => directory.upsert({ threadId, @@ -144,7 +160,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => runtimeMode: session.runtimeMode, status: toRuntimeStatus(session), ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), - runtimePayload: toRuntimePayloadFromSession(session), + runtimePayload: toRuntimePayloadFromSession(session, extra), }); const providers = yield* registry.listProviders(); @@ -197,11 +213,13 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => } const persistedCwd = readPersistedCwd(input.binding.runtimePayload); + const persistedProviderOptions = readPersistedProviderOptions(input.binding.runtimePayload); const resumed = yield* adapter.startSession({ threadId: input.binding.threadId, provider: input.binding.provider, ...(persistedCwd ? { cwd: persistedCwd } : {}), + ...(persistedProviderOptions ? { providerOptions: persistedProviderOptions } : {}), ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}), runtimeMode: input.binding.runtimeMode ?? "full-access", }); @@ -273,7 +291,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => ); } - yield* upsertSessionBinding(session, threadId); + yield* upsertSessionBinding(session, threadId, { + ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), + }); yield* analytics.record("provider.session.started", { provider: session.provider, runtimeMode: input.runtimeMode, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index c1693f4199..70e1c8fa02 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -830,6 +830,17 @@ export default function ChatView({ threadId }: ChatViewProps) { }; return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined; }, [selectedCodexFastModeEnabled, selectedEffort, selectedProvider, supportsReasoningEffort]); + const providerOptionsForDispatch = useMemo(() => { + if (!settings.codexBinaryPath && !settings.codexHomePath) { + return undefined; + } + return { + codex: { + ...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}), + ...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}), + }, + }; + }, [settings.codexBinaryPath, settings.codexHomePath]); const selectedModelForPicker = selectedModel; const modelOptionsByProvider = useMemo( () => getCustomModelOptionsByProvider(settings), @@ -2626,6 +2637,9 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), + ...(providerOptionsForDispatch + ? { providerOptions: providerOptionsForDispatch } + : {}), provider: selectedProvider, assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, @@ -2903,6 +2917,9 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), + ...(providerOptionsForDispatch + ? { providerOptions: providerOptionsForDispatch } + : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: nextInteractionMode, @@ -2933,6 +2950,7 @@ export default function ChatView({ threadId }: ChatViewProps) { runtimeMode, selectedModel, selectedModelOptionsForDispatch, + providerOptionsForDispatch, selectedProvider, setComposerDraftInteractionMode, setThreadError, @@ -2987,8 +3005,8 @@ export default function ChatView({ threadId }: ChatViewProps) { worktreePath: activeThread.worktreePath, createdAt, }) - .then(() => - api.orchestration.dispatchCommand({ + .then(() => { + return api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), threadId: nextThreadId, @@ -3003,12 +3021,15 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), + ...(providerOptionsForDispatch + ? { providerOptions: providerOptionsForDispatch } + : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: "default", createdAt, - }), - ) + }); + }) .then(() => api.orchestration.getSnapshot()) .then((snapshot) => { syncServerReadModel(snapshot); @@ -3052,6 +3073,7 @@ export default function ChatView({ threadId }: ChatViewProps) { runtimeMode, selectedModel, selectedModelOptionsForDispatch, + providerOptionsForDispatch, selectedProvider, settings.enableAssistantStreaming, syncServerReadModel, diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index aa7bd827de..e90ddd4b59 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -45,6 +45,13 @@ export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; export const ProviderServiceTier = Schema.Literals(["fast", "flex"]); export type ProviderServiceTier = typeof ProviderServiceTier.Type; export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; +const CodexProviderStartOptions = Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyString), + homePath: Schema.optional(TrimmedNonEmptyString), +}); +const ProviderStartOptions = Schema.Struct({ + codex: Schema.optional(CodexProviderStartOptions), +}); export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); export type RuntimeMode = typeof RuntimeMode.Type; export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; @@ -366,6 +373,7 @@ export const ThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), modelOptions: Schema.optional(ProviderModelOptions), + providerOptions: Schema.optional(ProviderStartOptions), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), interactionMode: ProviderInteractionMode.pipe( @@ -388,6 +396,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), modelOptions: Schema.optional(ProviderModelOptions), + providerOptions: Schema.optional(ProviderStartOptions), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, @@ -668,6 +677,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), modelOptions: Schema.optional(ProviderModelOptions), + providerOptions: Schema.optional(ProviderStartOptions), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), interactionMode: ProviderInteractionMode.pipe( diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 9ca7068ad3..5e551c5579 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -53,9 +53,10 @@ const CodexProviderStartOptions = Schema.Struct({ homePath: Schema.optional(TrimmedNonEmptyStringSchema), }); -const ProviderStartOptions = Schema.Struct({ +export const ProviderStartOptions = Schema.Struct({ codex: Schema.optional(CodexProviderStartOptions), }); +export type ProviderStartOptions = typeof ProviderStartOptions.Type; export const ProviderSessionStartInput = Schema.Struct({ threadId: ThreadId,