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
28 changes: 28 additions & 0 deletions .agents/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@ Two standing sections outlive any single batch and must be read on every review:

## Review batches

## 2026-09-02 — scoped adoption, cursor unchanged

**Not a batch review.** The developer asked for upstream's Claude Fable 5.1
support specifically, ahead of any other upstream work, so this session reviewed
exactly one change set out of the 129 commits sitting in
`9b2d04317c68233782e0630464ac86d77d0686f3..57a66608b918d673eeec7e6c94ea5906b756fcd0`.
The other 128 commits were **not** reviewed and have received no decision.

`reviewed-through` therefore stays at `9b2d0431`. A later full review must treat
the whole range as unreviewed apart from the row below, which it should skip as
already adopted.

| ID | Upstream | Decision | Pylon reference | Notes |
| --- | ---------------------------------------------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| S-1 | `c17d02cff` / `#9078` — Claude Fable 5.1 model | adopted | `#247` | Clean `cherry-pick -x`, no conflicts, applied line-for-line. Self-contained: it does not depend on `#9084` (remote manifest discovery), which is the large `ClaudeProvider.ts` restructure that follows it upstream and was deliberately left out of scope. |

Two consequences worth carrying forward. The bundled manifest swap promotes
`claude-fable-5-1` into the current Claude set and thereby marks
`claude-fable-5` **legacy** — that is upstream's intent, since 5.1 supersedes 5
in the same tier, but it is a user-visible change to an existing model's status.
And the catalog is gated on Claude Code `v2.1.257`, so the model stays hidden on
older CLIs rather than failing at spawn time.

`#9084` (`03542836`, remote Claude model manifest) is a separate 26-file
restructure that another session had begun stacking on the same branch. It was
split back out and is **not** adopted here; it remains undecided in the
unreviewed range above.

## 2026-09-01 — `1f8ed54add4133ac39effceded8fc1fff12d8e03..9b2d04317c68233782e0630464ac86d77d0686f3`

Twenty-nine upstream commits, reviewed as change sets C-1 through C-21. The
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/provider/Layers/ClaudeAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,29 @@ describe("ClaudeAdapterLive", () => {
);
});

it.effect("preserves xhigh effort for Claude Fable 5.1", () => {
const harness = makeHarness();
return Effect.gen(function* () {
const adapter = yield* ClaudeAdapter;
yield* adapter.startSession({
threadId: THREAD_ID,
provider: ProviderDriverKind.make("claudeAgent"),
modelSelection: createModelSelection(
ProviderInstanceId.make("claudeAgent"),
"claude-fable-5-1",
[{ id: "effort", value: "xhigh" }],
),
runtimeMode: "full-access",
});

const createInput = harness.getLastCreateQueryInput();
assert.equal(createInput?.options.effort, "xhigh");
}).pipe(
Effect.provideService(Random.Random, makeDeterministicRandomService()),
Effect.provide(harness.layer),
);
});

it.effect("preserves xhigh effort for Claude Fable 5", () => {
const harness = makeHarness();
return Effect.gen(function* () {
Expand Down
67 changes: 59 additions & 8 deletions apps/server/src/provider/Layers/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,48 @@ const CLAUDE_PRESENTATION = {
showInteractionModeToggle: true,
supportsConversationRollback: false,
} as const;
const MINIMUM_CLAUDE_FABLE_5_1_VERSION = "2.1.257";
const MINIMUM_CLAUDE_OPUS_5_VERSION = "2.1.219";
const MINIMUM_CLAUDE_FABLE_5_VERSION = "2.1.169";
const MINIMUM_CLAUDE_OPUS_4_8_VERSION = "2.1.154";
const MINIMUM_CLAUDE_OPUS_4_7_VERSION = "2.1.111";

const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
{
slug: "claude-fable-5-1",
name: "Claude Fable 5.1",
isCustom: false,
capabilities: createModelCapabilities({
optionDescriptors: [
buildSelectOptionDescriptor({
id: "effort",
label: "Reasoning",
options: [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High", isDefault: true },
{ value: "xhigh", label: "Extra High" },
{ value: "max", label: "Max" },
{
value: "ultracode",
label: "Ultracode",
description: "xhigh effort plus multi-agent workflow orchestration",
},
{ value: "ultrathink", label: "Ultrathink" },
],
promptInjectedValues: ["ultrathink"],
}),
buildSelectOptionDescriptor({
id: "contextWindow",
label: "Context Window",
options: [
{ value: "200k", label: "200k" },
{ value: "1m", label: "1M", isDefault: true },
],
}),
],
}),
},
{
slug: "claude-fable-5",
name: "Claude Fable 5",
Expand Down Expand Up @@ -330,6 +366,10 @@ const CLAUDE_MODEL_CATALOG: ReadonlyArray<ServerProviderModel> = [
// so the catalog itself carries no `isLegacy` flags.
const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = CLAUDE_MODEL_CATALOG;

function supportsClaudeFable51(version: string | null | undefined): boolean {
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_FABLE_5_1_VERSION) >= 0 : false;
}

function supportsClaudeOpus5(version: string | null | undefined): boolean {
return version ? compareSemverVersions(version, MINIMUM_CLAUDE_OPUS_5_VERSION) >= 0 : false;
}
Expand All @@ -350,6 +390,9 @@ function getBuiltInClaudeModelsForVersion(
version: string | null | undefined,
): ReadonlyArray<ServerProviderModel> {
return BUILT_IN_MODELS.filter((model) => {
if (model.slug === "claude-fable-5-1") {
return supportsClaudeFable51(version);
}
if (model.slug === "claude-opus-5") {
return supportsClaudeOpus5(version);
}
Expand All @@ -366,6 +409,11 @@ function getBuiltInClaudeModelsForVersion(
});
}

function formatClaudeFable51UpgradeMessage(version: string | null): string {
const versionLabel = version ? `v${version}` : "the installed version";
return `Claude Code ${versionLabel} is too old for Claude Fable 5.1. Upgrade to v${MINIMUM_CLAUDE_FABLE_5_1_VERSION} or newer to access it.`;
}

function formatClaudeOpus5UpgradeMessage(version: string | null): string {
const versionLabel = version ? `v${version}` : "the installed version";
return `Claude Code ${versionLabel} is too old for Claude Opus 5. Upgrade to v${MINIMUM_CLAUDE_OPUS_5_VERSION} or newer to access it.`;
Expand Down Expand Up @@ -429,6 +477,7 @@ export function normalizeClaudeCliEffort(
}
if (
effort === "xhigh" &&
model !== "claude-fable-5-1" &&
model !== "claude-fable-5" &&
model !== "claude-opus-5" &&
model !== "claude-opus-4-8" &&
Expand Down Expand Up @@ -1026,15 +1075,17 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
claudeSettings.customModels,
DEFAULT_CLAUDE_MODEL_CAPABILITIES,
);
const versionUpgradeMessage = supportsClaudeOpus5(parsedVersion)
const versionUpgradeMessage = supportsClaudeFable51(parsedVersion)
? undefined
: supportsClaudeFable5(parsedVersion)
? formatClaudeOpus5UpgradeMessage(parsedVersion)
: supportsClaudeOpus48(parsedVersion)
? formatClaudeFable5UpgradeMessage(parsedVersion)
: supportsClaudeOpus47(parsedVersion)
? formatClaudeOpus48UpgradeMessage(parsedVersion)
: formatClaudeOpus47UpgradeMessage(parsedVersion);
: supportsClaudeOpus5(parsedVersion)
? formatClaudeFable51UpgradeMessage(parsedVersion)
: supportsClaudeFable5(parsedVersion)
? formatClaudeOpus5UpgradeMessage(parsedVersion)
: supportsClaudeOpus48(parsedVersion)
? formatClaudeFable5UpgradeMessage(parsedVersion)
: supportsClaudeOpus47(parsedVersion)
? formatClaudeOpus48UpgradeMessage(parsedVersion)
: formatClaudeOpus47UpgradeMessage(parsedVersion);

const capabilities = resolveCapabilities
? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
Expand Down
56 changes: 56 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2740,6 +2740,62 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
),
);

it.effect("includes Claude Fable 5.1 on supported Claude Code versions", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
defaultClaudeSettings,
claudeCapabilities(),
);
const fable51 = status.models.find((model) => model.slug === "claude-fable-5-1");
assert.strictEqual(fable51?.name, "Claude Fable 5.1");
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "2.1.257\n", stderr: "", code: 0 };
if (joined === "auth status")
return {
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
stderr: "",
code: 0,
};
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("hides Claude Fable 5.1 on older Claude Code versions", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
defaultClaudeSettings,
claudeCapabilities(),
);
assert.strictEqual(
status.models.some((model) => model.slug === "claude-fable-5-1"),
false,
);
assert.strictEqual(
status.message,
"Claude Code v2.1.256 is too old for Claude Fable 5.1. Upgrade to v2.1.257 or newer to access it.",
);
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "2.1.256\n", stderr: "", code: 0 };
if (joined === "auth status")
return {
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
stderr: "",
code: 0,
};
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("hides Claude Fable 5 on older Claude Code versions", () =>
Effect.gen(function* () {
const status = yield* checkClaudeProviderStatus(
Expand Down
14 changes: 9 additions & 5 deletions apps/server/src/provider/ModelManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ describe("isLegacyModel (bundled manifest)", () => {

it("keeps only the Claude 5 family out of legacy models", () => {
assert.deepStrictEqual(
["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4-8"].map((model) => [
model,
isLegacyModel(BUNDLED_MODEL_MANIFEST, CLAUDE, model),
]),
[
["claude-fable-5", false],
"claude-fable-5-1",
"claude-fable-5",
"claude-opus-5",
"claude-sonnet-5",
"claude-opus-4-8",
].map((model) => [model, isLegacyModel(BUNDLED_MODEL_MANIFEST, CLAUDE, model)]),
[
["claude-fable-5-1", false],
["claude-fable-5", true],
["claude-opus-5", false],
["claude-sonnet-5", false],
["claude-opus-4-8", true],
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/model-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"gpt-daybreak-blue-latest",
"gpt-daybreak-red-latest"
],
"claudeAgent": ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]
"claudeAgent": ["claude-fable-5-1", "claude-opus-5", "claude-sonnet-5"]
}
}
3 changes: 3 additions & 0 deletions packages/contracts/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Partial<
"gpt-5.3-spark": "gpt-5.3-codex-spark",
},
[CLAUDE_DRIVER_KIND]: {
fable: "claude-fable-5-1",
"fable-5.1": "claude-fable-5-1",
"claude-fable-5.1": "claude-fable-5-1",
opus: "claude-opus-5",
"opus-5": "claude-opus-5",
"claude-opus-5.0": "claude-opus-5",
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ describe("model slug normalization", () => {
it("preserves exact custom slugs instead of expanding provider aliases", () => {
const claude = ProviderDriverKind.make("claudeAgent");

expect(normalizeModelSlug("fable", claude)).toBe("claude-fable-5-1");
expect(normalizeModelSlug("opus", claude)).toBe("claude-opus-5");
expect(normalizeCustomModelSlug(" opus ")).toBe("opus");
});
Expand Down
Loading