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
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export const make = Effect.gen(function* () {
sourceControlSshPasswordPrompts: true,
providerHandoff: true,
threadMessageCorrection: true,
modelRouting: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
...(desktopMcpPath === undefined ? {} : { computerView: true }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ const make = Effect.gen(function* () {
provider: "mt",
method: "thread.turn.start",
detail:
"MT Model has no ready providers to route to. Enable Codex, Claude, Cursor, or another provider in Settings.",
"MT Auto has no ready providers to route to. Enable Codex, Claude, Cursor, or another provider in Settings.",
});
}
const resolveActiveSession = (threadId: ThreadId) =>
Expand Down Expand Up @@ -683,7 +683,7 @@ const make = Effect.gen(function* () {
tone: "info",
kind: "thread.model-changed",
summary: isMtModelSelection(stickyModelSelection)
? (mtDecision?.reason ?? `MT Model → ${desiredModelSelection.model}`)
? (mtDecision?.reason ?? `MT Auto → ${desiredModelSelection.model}`)
: `Switched model to ${desiredModelSelection.model}`,
payload: {
fromInstanceId: String(currentInstanceId),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/mtModelCloudflare.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @effect-diagnostics globalTimersInEffect:off - the classifier races a raw fetch against a deadline.
// @effect-diagnostics preferSchemaOverJson:off - the worker response is untyped JSON, validated by hand below.
/**
* Optional Cloudflare classifier for MT Model.
* Optional Cloudflare classifier for MT Auto.
*
* MT Code matches T3 Code: local and $0 by default. This client stays dark
* unless `MT_MODEL_ROUTER_URL` is set to an explicit worker URL. We never
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ export function deriveLockedProvider(input: {
if (!threadHasStarted(input.thread)) {
return null;
}
// MT Model is a harness router. The live session sits on whatever backend
// MT Auto is a harness router. The live session sits on whatever backend
// it picked; locking to that backend would steal the picker away from MT.
if (isMtModelSelection(input.thread?.modelSelection)) {
return null;
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2273,7 +2273,14 @@ function ChatViewContent(props: ChatViewProps) {
versionMismatchServerLabel,
]);
const providerStatuses = serverConfig?.providers ?? EMPTY_PROVIDERS;
const pickerProviders = useMemo(() => withMtModelProvider(providerStatuses), [providerStatuses]);
// Only the machine that will run the turn can route it; an older server
// rejects the `mt` instance instead of picking a backend.
const environmentCanRouteModels =
activeEnvironment?.serverConfig?.environment.capabilities.modelRouting !== false;
const pickerProviders = useMemo(
() => withMtModelProvider(providerStatuses, { serverCanRoute: environmentCanRouteModels }),
[environmentCanRouteModels, providerStatuses],
);
const unlockedSelectedProvider = resolveSelectableProvider(
providerStatuses,
selectedProviderByThreadId ?? threadProvider,
Expand Down
20 changes: 17 additions & 3 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ import type { ReviewCommentContext } from "../../reviewCommentContext";
import { useThreadShells } from "../../state/entities";
import { searchThreadReferences } from "../../threadReferenceSearch";

import { useEnvironmentPresentation } from "~/state/presentation";

const runtimeModeConfig: Record<
RuntimeMode,
{ label: string; description: string; icon: LucideIcon }
Expand Down Expand Up @@ -803,16 +805,28 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
// Instance-aware projection of the wire provider list. One entry per
// configured instance (default built-in + any custom `providerInstances.*`),
// sorted default-first per driver kind for a stable picker order.
// The turn runs on the thread's machine, and only a machine that understands
// the `mt` instance can route it. An older server answers with "unknown
// provider instance 'mt'", so the entry is withheld for those environments.
const { presentation: composerEnvironmentPresentation } = useEnvironmentPresentation(
_activeThreadEnvironmentId ?? environmentId,
);
const serverCanRoute =
composerEnvironmentPresentation?.serverConfig?.environment.capabilities.modelRouting !== false;
const providerInstanceEntries = useMemo<ReadonlyArray<ProviderInstanceEntry>>(
() =>
prependMtModelPickerEntry(
sortProviderInstanceEntries(
applyProviderInstanceSettings(deriveProviderInstanceEntries(providerStatuses), settings),
),
{ serverCanRoute },
),
[providerStatuses, settings],
[providerStatuses, serverCanRoute, settings],
);
const pickerProviders = useMemo(
() => withMtModelProvider(providerStatuses, { serverCanRoute }),
[providerStatuses, serverCanRoute],
);
const pickerProviders = useMemo(() => withMtModelProvider(providerStatuses), [providerStatuses]);
const selectedProviderByThreadId = composerDraft.activeProvider ?? null;
const threadProvider =
activeThread?.session?.providerInstanceId ??
Expand Down Expand Up @@ -857,7 +871,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
// 5. First enabled entry overall / default instance for the kind.
//
const selectedInstanceId = useMemo<ProviderInstanceId>(() => {
// Session instance is the *routed* backend after MT Model picks one.
// Session instance is the *routed* backend after MT Auto picks one.
// Keep the sticky MT selection in the picker unless the draft explicitly
// moved to another provider.
if (
Expand Down
47 changes: 43 additions & 4 deletions apps/web/src/mtModel.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vite-plus/test";
import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";

import { prependMtModelPickerEntry, withMtModelProvider } from "./mtModel.ts";

describe("withMtModelProvider", () => {
it("prepends MT Model when another provider is ready", () => {
it("prepends MT Auto when another provider is ready", () => {
const providers = withMtModelProvider([
{
instanceId: ProviderInstanceId.make("claudeAgent"),
Expand Down Expand Up @@ -32,7 +32,7 @@ describe("withMtModelProvider", () => {
expect(providers[0]?.models[0]?.slug).toBe("mt-auto");
});

it("hides MT Model when no backend is ready", () => {
it("hides MT Auto when no backend is ready", () => {
expect(
withMtModelProvider([
{
Expand All @@ -52,7 +52,7 @@ describe("withMtModelProvider", () => {
).toBe(false);
});

it("still offers MT Model when the only backend is in warning", () => {
it("still offers MT Auto when the only backend is in warning", () => {
expect(
withMtModelProvider([
{
Expand All @@ -79,4 +79,43 @@ describe("withMtModelProvider", () => {
])[0]?.instanceId,
).toBe("mt");
});

it("withholds the router from a machine whose server cannot route", () => {
// An older server answers a routed turn with "unknown provider instance
// 'mt'", so offering it there is a guaranteed failure.
const backends = [
{
instanceId: ProviderInstanceId.make("claudeAgent"),
driver: ProviderDriverKind.make("claudeAgent"),
enabled: true,
installed: true,
version: null,
status: "ready",
auth: { status: "unknown" },
checkedAt: "2026-08-18T00:00:00.000Z",
models: [
{
slug: "claude-sonnet-5",
name: "Sonnet 5",
isCustom: false,
isDefault: true,
capabilities: null,
},
],
slashCommands: [],
skills: [],
},
] satisfies ReadonlyArray<ServerProvider>;
expect(
withMtModelProvider(backends, { serverCanRoute: false }).map(
(provider) => provider.instanceId,
),
).not.toContain("mt");
expect(
withMtModelProvider(backends, { serverCanRoute: true }).map(
(provider) => provider.instanceId,
),
).toContain("mt");
});

});
16 changes: 15 additions & 1 deletion apps/web/src/mtModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function createMtModelCapabilities() {
id: MT_MODEL_ROUTE_MODE_OPTION_ID,
label: "Routing",
description:
"How aggressively MT Model spends quota on frontier models. Cost keeps chores cheap; Intelligence upgrades harder tasks. Routing is local and free.",
"How aggressively MT Auto spends quota on frontier models. Cost keeps chores cheap; Intelligence upgrades harder tasks. Routing is local and free.",
type: "select",
currentValue: DEFAULT_MT_MODEL_ROUTE_MODE,
options: [
Expand Down Expand Up @@ -80,12 +80,22 @@ export function createMtModelProviderSnapshot(): ServerProvider {
};
}

/**
* MT Auto is a harness-level router: the client offers it, but the SERVER has
* to understand the `mt` instance when the turn starts. A machine running an
* older build rejects it outright ("references unknown provider instance
* 'mt'"), so the entry is withheld unless that environment says it can route.
*/
export function withMtModelProvider(
providers: ReadonlyArray<ServerProvider>,
options?: { readonly serverCanRoute?: boolean | undefined },
): ReadonlyArray<ServerProvider> {
if (providers.some((provider) => isMtModelInstanceId(provider.instanceId))) {
return providers;
}
if (options?.serverCanRoute === false) {
return providers;
}
const hasReadyBackend = providers.some(
(provider) =>
!isMtModelInstanceId(provider.instanceId) &&
Expand All @@ -101,10 +111,14 @@ export function withMtModelProvider(

export function prependMtModelPickerEntry(
entries: ReadonlyArray<ProviderInstanceEntry>,
options?: { readonly serverCanRoute?: boolean | undefined },
): ReadonlyArray<ProviderInstanceEntry> {
if (entries.some((entry) => isMtModelInstanceId(entry.instanceId))) {
return entries;
}
if (options?.serverCanRoute === false) {
return entries;
}
const hasReadyBackend = entries.some(
(entry) =>
!isMtModelInstanceId(entry.instanceId) &&
Expand Down
4 changes: 4 additions & 0 deletions packages/contracts/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export const ExecutionEnvironmentCapabilities = Schema.Struct({
/** Server can start a fresh provider session and replay bounded thread
context when a started thread switches to an incompatible provider. */
providerHandoff: Schema.optionalKey(Schema.Boolean),
/** Server understands the `mt` router instance and picks a real backend per
turn. Absent on servers from before MT Auto, which reject the instance
outright - so clients must not offer it for those environments. */
modelRouting: Schema.optionalKey(Schema.Boolean),
/** Server understands thread.message.correct. Absent on older servers, so
clients hide the action instead of sending it. */
threadMessageCorrection: Schema.optionalKey(Schema.Boolean),
Expand Down
4 changes: 2 additions & 2 deletions packages/contracts/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ const ANTIGRAVITY_DRIVER_KIND = ProviderDriverKind.make("antigravity");
export const MT_MODEL_DRIVER_KIND = ProviderDriverKind.make("mt");
export const MT_MODEL_INSTANCE_ID = ProviderInstanceId.make("mt");
export const MT_MODEL_SLUG = "mt-auto";
export const MT_MODEL_DISPLAY_NAME = "MT Model";
export const MT_MODEL_PROVIDER_LABEL = "MT Code";
export const MT_MODEL_DISPLAY_NAME = "MT Auto";
export const MT_MODEL_PROVIDER_LABEL = "Munim";
export const MT_MODEL_ROUTE_MODE_OPTION_ID = "routeMode";
export const MT_MODEL_ROUTE_MODES = ["cost", "balance", "intelligence"] as const;
export type MtModelRouteMode = (typeof MT_MODEL_ROUTE_MODES)[number];
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/mtModelRouter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* MT Model router — local analog of Cursor Auto / Cursor Router.
* MT Auto router — local analog of Cursor Auto / Cursor Router.
*
* Cursor trains Compass on hundreds of thousands of live turns. We score each
* turn locally (same $0 model as T3 Code: no cloud classifier). The resolved
Expand Down Expand Up @@ -408,7 +408,7 @@ function describeRoute(
if (classification.source === "cloudflare") {
parts.push("cloudflare");
}
return `MT Model → ${candidate.model} (${parts.join(", ")})`;
return `MT Auto → ${candidate.model} (${parts.join(", ")})`;
}

function unwrapClassificationRecord(input: unknown): Record<string, unknown> | null {
Expand Down
Loading