Skip to content
Merged
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
5 changes: 4 additions & 1 deletion apps/server/src/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ const PROVIDER_LABELS: Record<string, string> = {
codex: "Codex (OpenAI)",
claudeAgent: "Claude Code",
copilot: "GitHub Copilot",
gemini: "Gemini CLI",
openclaw: "OpenClaw",
};

function printStatus(status: ServerProviderStatus): void {
const icon = STATUS_ICONS[status.status] ?? "?";
const label = PROVIDER_LABELS[status.provider] ?? status.provider;
const auth = AUTH_LABELS[status.authStatus] ?? status.authStatus;
const authStatus = status.authStatus ?? status.auth?.status ?? "unknown";
const auth = AUTH_LABELS[authStatus] ?? authStatus;

console.log("");
console.log(` ${icon} ${label}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ const makeProjectionOverviewQuery = Effect.gen(function* () {
p.title,
p.workspace_root AS "workspaceRoot",
p.default_model AS "defaultModel",
p.default_model_selection AS "defaultModelSelection",
p.scripts_json AS "scripts",
p.created_at AS "createdAt",
p.updated_at AS "updatedAt",
Expand All @@ -209,6 +210,7 @@ const makeProjectionOverviewQuery = Effect.gen(function* () {
t.project_id AS "projectId",
t.title,
t.model,
t.model_selection AS "modelSelection",
t.runtime_mode AS "runtimeMode",
t.interaction_mode AS "interactionMode",
t.branch,
Expand Down Expand Up @@ -361,6 +363,7 @@ const makeProjectionOverviewQuery = Effect.gen(function* () {
title: row.title,
workspaceRoot: row.workspaceRoot,
defaultModel: row.defaultModel,
defaultModelSelection: row.defaultModelSelection,
scripts: row.scripts,
activeThreadCount: row.activeThreadCount,
createdAt: row.createdAt,
Expand All @@ -374,6 +377,7 @@ const makeProjectionOverviewQuery = Effect.gen(function* () {
projectId: row.projectId,
title: row.title,
model: row.model,
modelSelection: row.modelSelection,
runtimeMode: row.runtimeMode,
interactionMode: row.interactionMode,
branch: row.branch,
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
title: event.payload.title,
workspaceRoot: event.payload.workspaceRoot,
defaultModel: event.payload.defaultModel,
defaultModelSelection: event.payload.defaultModelSelection,
scripts: event.payload.scripts,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
Expand All @@ -421,6 +422,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
...(event.payload.defaultModel !== undefined
? { defaultModel: event.payload.defaultModel }
: {}),
...(event.payload.defaultModelSelection !== undefined
? { defaultModelSelection: event.payload.defaultModelSelection }
: {}),
...(event.payload.scripts !== undefined ? { scripts: event.payload.scripts } : {}),
updatedAt: event.payload.updatedAt,
});
Expand Down Expand Up @@ -456,6 +460,7 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
projectId: event.payload.projectId,
title: event.payload.title,
model: event.payload.model,
modelSelection: event.payload.modelSelection,
runtimeMode: event.payload.runtimeMode,
interactionMode: event.payload.interactionMode,
branch: event.payload.branch,
Expand All @@ -479,6 +484,9 @@ const makeOrchestrationProjectionPipeline = Effect.gen(function* () {
...existingRow.value,
...(event.payload.title !== undefined ? { title: event.payload.title } : {}),
...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
...(event.payload.modelSelection !== undefined
? { modelSelection: event.payload.modelSelection }
: {}),
...(event.payload.branch !== undefined ? { branch: event.payload.branch } : {}),
...(event.payload.worktreePath !== undefined
? { worktreePath: event.payload.worktreePath }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
title,
workspace_root AS "workspaceRoot",
default_model AS "defaultModel",
default_model_selection AS "defaultModelSelection",
scripts_json AS "scripts",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand All @@ -172,6 +173,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
project_id AS "projectId",
title,
model,
model_selection AS "modelSelection",
runtime_mode AS "runtimeMode",
interaction_mode AS "interactionMode",
branch,
Expand Down Expand Up @@ -549,6 +551,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
title: row.title,
workspaceRoot: row.workspaceRoot,
defaultModel: row.defaultModel,
defaultModelSelection: row.defaultModelSelection,
scripts: row.scripts,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand All @@ -563,6 +566,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
projectId: row.projectId,
title: row.title,
model: row.model,
modelSelection: row.modelSelection,
runtimeMode: row.runtimeMode,
interactionMode: row.interactionMode,
branch: row.branch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ const makeProjectionThreadDetailQuery = Effect.gen(function* () {
projectId: threadRow.value.projectId,
title: threadRow.value.title,
model: threadRow.value.model,
modelSelection: threadRow.value.modelSelection,
runtimeMode: threadRow.value.runtimeMode,
interactionMode: threadRow.value.interactionMode,
branch: threadRow.value.branch,
Expand Down
74 changes: 66 additions & 8 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CommandId,
DEFAULT_GIT_TEXT_GENERATION_MODEL,
EventId,
type ModelSelection,
type OrchestrationEvent,
type ProjectId,
type ProviderModelOptions,
Expand All @@ -30,6 +31,12 @@ import {
type ProviderCommandReactorShape,
} from "../Services/ProviderCommandReactor.ts";
import { inferProviderForModel } from "@okcode/shared/model";
import {
getModelSelectionModel,
getModelSelectionOptions,
getModelSelectionProvider,
toCanonicalModelSelection,
} from "@okcode/shared/modelSelection";
import { resolveRuntimeEnvironment } from "../../runtimeEnvironment.ts";

type ProviderIntentEvent = Extract<
Expand Down Expand Up @@ -68,6 +75,16 @@ function mapProviderSessionStatusToOrchestrationStatus(
}
}

function resolveThreadModelSelection(thread: {
readonly model: string;
readonly modelSelection?: ModelSelection | null | undefined;
}): ModelSelection {
return (
thread.modelSelection ??
toCanonicalModelSelection(inferProviderForModel(thread.model), thread.model, undefined)
);
}

const turnStartKeyForEvent = (event: ProviderIntentEvent): string =>
event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`;

Expand Down Expand Up @@ -257,6 +274,7 @@ const make = Effect.gen(function* () {
threadId: ThreadId,
createdAt: string,
options?: {
readonly modelSelection?: ModelSelection;
readonly provider?: ProviderKind;
readonly model?: string;
readonly modelOptions?: ProviderModelOptions;
Expand All @@ -275,17 +293,38 @@ const make = Effect.gen(function* () {
)
? thread.session.providerName
: undefined;
const threadProvider: ProviderKind = currentProvider ?? inferProviderForModel(thread.model);
const threadSelection = resolveThreadModelSelection(thread);
const requestedSelection =
options?.modelSelection ??
(options?.provider || options?.model || options?.modelOptions
? toCanonicalModelSelection(
options?.provider ?? threadSelection.provider,
options?.model ?? threadSelection.model,
options?.modelOptions ?? getModelSelectionOptions(threadSelection),
)
: threadSelection);
const threadProvider: ProviderKind =
currentProvider ?? getModelSelectionProvider(threadSelection);
if (options?.provider !== undefined && options.provider !== threadProvider) {
return yield* new ProviderAdapterRequestError({
provider: threadProvider,
method: "thread.turn.start",
detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${options.provider}'.`,
});
}
if (
options?.modelSelection !== undefined &&
getModelSelectionProvider(options.modelSelection) !== threadProvider
) {
return yield* new ProviderAdapterRequestError({
provider: threadProvider,
method: "thread.turn.start",
detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot use model selection for '${getModelSelectionProvider(options.modelSelection)}'.`,
});
}
if (
options?.model !== undefined &&
inferProviderForModel(options.model, threadProvider) !== threadProvider
getModelSelectionProvider(requestedSelection) !== threadProvider
) {
return yield* new ProviderAdapterRequestError({
provider: threadProvider,
Expand All @@ -294,7 +333,9 @@ const make = Effect.gen(function* () {
});
}
const preferredProvider: ProviderKind = currentProvider ?? threadProvider;
const desiredModel = options?.model ?? thread.model;
const desiredModel = getModelSelectionModel(requestedSelection);
const desiredModelOptions =
options?.modelOptions ?? getModelSelectionOptions(requestedSelection);
const { cwd: effectiveCwd, staleWorktreePath } = resolveSessionCwd({
thread,
projects: readModel.projects,
Expand Down Expand Up @@ -330,7 +371,7 @@ const make = Effect.gen(function* () {
: {}),
...(effectiveCwd ? { cwd: effectiveCwd } : {}),
...(desiredModel ? { model: desiredModel } : {}),
...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}),
...(desiredModelOptions !== undefined ? { modelOptions: desiredModelOptions } : {}),
...(options?.providerOptions !== undefined
? { providerOptions: options.providerOptions }
: {}),
Expand Down Expand Up @@ -376,8 +417,8 @@ const make = Effect.gen(function* () {
const previousModelOptions = threadModelOptions.get(threadId);
const shouldRestartForModelOptionsChange =
currentProvider === "claudeAgent" &&
options?.modelOptions !== undefined &&
!sameModelOptions(previousModelOptions, options.modelOptions);
desiredModelOptions !== undefined &&
!sameModelOptions(previousModelOptions, desiredModelOptions);
const activeSessionCwd = activeSession?.cwd;
const shouldRestartForCwdChange =
staleWorktreePath !== null || activeSessionCwd !== effectiveCwd;
Expand Down Expand Up @@ -441,6 +482,7 @@ const make = Effect.gen(function* () {
readonly messageText: string;
readonly providerInput?: string;
readonly attachments?: ReadonlyArray<ChatAttachment>;
readonly modelSelection?: ModelSelection;
readonly provider?: ProviderKind;
readonly model?: string;
readonly modelOptions?: ProviderModelOptions;
Expand All @@ -453,6 +495,7 @@ const make = Effect.gen(function* () {
return;
}
yield* ensureSessionForThread(input.threadId, input.createdAt, {
...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
...(input.provider !== undefined ? { provider: input.provider } : {}),
...(input.model !== undefined ? { model: input.model } : {}),
...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
Expand All @@ -463,6 +506,11 @@ const make = Effect.gen(function* () {
}
if (input.modelOptions !== undefined) {
threadModelOptions.set(input.threadId, input.modelOptions);
} else if (input.modelSelection?.options) {
const selectionOptions = getModelSelectionOptions(input.modelSelection);
if (selectionOptions !== undefined) {
threadModelOptions.set(input.threadId, selectionOptions);
}
}
const normalizedInput = toNonEmptyProviderInput(input.providerInput ?? input.messageText);
const normalizedAttachments = input.attachments ?? [];
Expand All @@ -476,13 +524,20 @@ const make = Effect.gen(function* () {
? "in-session"
: (yield* providerService.getCapabilities(activeSession.provider)).sessionModelSwitch;
const modelForTurn = sessionModelSwitch === "unsupported" ? activeSession?.model : input.model;
const requestedModelForTurn = input.modelSelection
? getModelSelectionModel(input.modelSelection)
: modelForTurn;
const requestedModelOptionsForTurn =
input.modelOptions ?? getModelSelectionOptions(input.modelSelection);

yield* providerService.sendTurn({
threadId: input.threadId,
...(normalizedInput ? { input: normalizedInput } : {}),
...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}),
...(modelForTurn !== undefined ? { model: modelForTurn } : {}),
...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
...(requestedModelForTurn !== undefined ? { model: requestedModelForTurn } : {}),
...(requestedModelOptionsForTurn !== undefined
? { modelOptions: requestedModelOptionsForTurn }
: {}),
...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}),
});
});
Expand Down Expand Up @@ -598,6 +653,9 @@ const make = Effect.gen(function* () {
? { providerInput: event.payload.providerInput }
: {}),
...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
...(event.payload.modelSelection != null
? { modelSelection: event.payload.modelSelection }
: {}),
...(event.payload.provider !== undefined ? { provider: event.payload.provider } : {}),
...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
...(event.payload.modelOptions !== undefined
Expand Down
43 changes: 43 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
ThreadId,
} from "@okcode/contracts";
import { Effect } from "effect";
import { inferProviderForModel } from "@okcode/shared/model";
import { toCanonicalModelSelection } from "@okcode/shared/modelSelection";

import { OrchestrationCommandInvariantError } from "./Errors.ts";
import {
Expand Down Expand Up @@ -127,6 +129,15 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
title: command.title,
workspaceRoot: command.workspaceRoot,
defaultModel: command.defaultModel ?? null,
defaultModelSelection:
command.defaultModelSelection ??
(command.defaultModel
? toCanonicalModelSelection(
inferProviderForModel(command.defaultModel),
command.defaultModel,
undefined,
)
: null),
scripts: command.scripts ?? [],
createdAt: command.createdAt,
updatedAt: command.createdAt,
Expand Down Expand Up @@ -184,6 +195,17 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.title !== undefined ? { title: command.title } : {}),
...(command.workspaceRoot !== undefined ? { workspaceRoot: command.workspaceRoot } : {}),
...(command.defaultModel !== undefined ? { defaultModel: command.defaultModel } : {}),
...(command.defaultModelSelection !== undefined
? { defaultModelSelection: command.defaultModelSelection }
: command.defaultModel !== undefined
? {
defaultModelSelection: toCanonicalModelSelection(
inferProviderForModel(command.defaultModel),
command.defaultModel,
undefined,
),
}
: {}),
...(command.scripts !== undefined ? { scripts: command.scripts } : {}),
updatedAt: occurredAt,
},
Expand Down Expand Up @@ -245,6 +267,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
projectId: command.projectId,
title: command.title,
model: command.model,
modelSelection:
command.modelSelection ??
toCanonicalModelSelection(
inferProviderForModel(command.model),
command.model,
undefined,
),
runtimeMode: command.runtimeMode,
interactionMode: command.interactionMode,
branch: command.branch,
Expand Down Expand Up @@ -306,6 +335,17 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
threadId: command.threadId,
...(command.title !== undefined ? { title: command.title } : {}),
...(command.model !== undefined ? { model: command.model } : {}),
...(command.modelSelection !== undefined
? { modelSelection: command.modelSelection }
: command.model !== undefined
? {
modelSelection: toCanonicalModelSelection(
inferProviderForModel(command.model),
command.model,
undefined,
),
}
: {}),
...(command.branch !== undefined ? { branch: command.branch } : {}),
...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}),
...(command.githubRef !== undefined ? { githubRef: command.githubRef } : {}),
Expand Down Expand Up @@ -423,6 +463,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
threadId: command.threadId,
messageId: command.message.messageId,
...(command.providerInput !== undefined ? { providerInput: command.providerInput } : {}),
...(command.modelSelection !== undefined
? { modelSelection: command.modelSelection }
: {}),
...(command.provider !== undefined ? { provider: command.provider } : {}),
...(command.model !== undefined ? { model: command.model } : {}),
...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}),
Expand Down
Loading