feat(adapters): refresh current coding models and reasoning controls (#13829)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Its adapters supply model catalogs and reasoning controls to agent
setup.
> - Several provider releases are missing from the fallback catalogs.
> - Some newer models also have effort levels that the UI does not
offer.
> - Operators need the exact supported IDs and controls when discovery
is unavailable.
> - This pull request updates the existing adapters from current
provider documentation.
> - Operators can select current coding models without entering custom
IDs.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Model selection and reasoning controls across the existing coding-agent
adapters.

**Subsystem affected**

Claude, Codex, Grok, Gemini, Cursor, Kimi, and OpenCode adapters; model
discovery tests; agent creation and editing.

**Current behavior**

The catalogs omit Opus 5.5, GPT-6 Sol/Luna, Grok 4.7/4.6/4.5, current
Gemini Flash models, and several Cursor/Kimi choices. Bedrock has
obsolete IDs. The UI omits supported effort levels and saves Grok effort
under a key the runtime does not read.

**Proposed behavior**

Offer verified current model IDs and model-specific efforts. Remove
retired Gemini 2.0 choices. Keep configured defaults and saved model
IDs. Keep runtime discovery for account-specific choices.

**Reason and benefit**

Catch up with provider releases through September 22, 2026. Correct the
picker and runtime controls together.

**Breaking changes**

No database or API change. Gemini 2.0 options leave the picker after
their June 1 shutdown. Existing saved IDs remain unchanged. Corrected
Bedrock catalog IDs do not rewrite saved configuration.

**Additional context**

Supersedes the separate GPT-6 Sol PR #13830. Fable 5.1 was already
merged in #12730, and GPT-6 Astra in #12851. The Grok 4.6/4.5 proposal
#11324 was closed and parked by its author. This change retains the
default-sentinel fix from #12062. Related discovery proposals #13127 and
#13565 do not supply these catalog and effort updates. Searches found no
open PR for the additional model IDs.

See [the dated
audit](https://github.com/paperclipai/paperclip/blob/feat/claude-opus-5-5/doc/adapter-model-audit-2026-09-22.md)
for exact scope, primary sources, runtime observations, and
account-specific limits. This updates existing adapters and does not
duplicate planned core work.

## What Changed

- Add Opus 5.5 for direct Claude and Bedrock, with a Claude Code 2.1.280
gate. Correct and extend Bedrock model IDs.
- Add GPT-6 Sol/Luna and Fast mode. Offer Ultra for Astra/Sol and
GPT-5.6 Sol/Terra, and Max for both Luna generations.
- Add Grok 4.7/4.6/4.5, expose supported Extra High effort, and save
Grok edits under `reasoningEffort`.
- Add Gemini Flash 3.8/3.7/3.6/3.5, Flash Lite 3.5/3.1, and 3 Flash
Preview. Remove retired 2.0 choices.
- Add the current documented Cursor fallback models, including Fable
5.1, Composer 2.5, and Muse Spark 1.3.
- Refresh OpenCode fallback IDs used in remote environments from its
installed provider registry.
- Add Kimi K3 256K. Update the existing coding alias to K2.8 Preview and
enable its CLI effort settings.
- Use model-specific Claude/Grok efforts in creation and editing. Clear
unsupported effort when switching models.
- Add catalog, CLI/ACP forwarding, compatibility, and UI persistence
coverage. Record the audit and sources.

## Verification

- Latest head `6e63c9ef53b54ba869cd4fb431a8570bebe289f4`: 53 CI checks
passed, 2 skipped. This includes full workspace typecheck, build, and
all test shards. Greptile is 5/5 with zero unresolved threads. GitHub
reports no merge conflicts.
- 340 focused tests passed across adapter metadata, CLI/ACP arguments,
Claude version checks, Kimi effort, Grok execution, server model
discovery, and UI effort selection/persistence.
- `pnpm --filter @paperclipai/adapter-claude-local --filter
@paperclipai/adapter-codex-local --filter
@paperclipai/adapter-grok-local --filter
@paperclipai/adapter-gemini-local --filter
@paperclipai/adapter-kimi-local --filter
@paperclipai/adapter-cursor-local --filter
@paperclipai/adapter-opencode-local typecheck` — passed. The same
filters with `build` passed.
- `pnpm check:token-gates` and `git diff --check` — passed.
- Full workspace and UI typechecks were attempted locally. They stop on
existing missing `three` dependencies in `packages/shared/src/cliplab`.
- Full `pnpm test:run` and `pnpm build` were not run locally. Worktree
creation exhausted disk space, so a clean dependency install is not
feasible on this host. Focused checks reuse existing dependencies. CI
supplies full workspace verification.
- No provider inference was run. Account-specific runtime model lists
were inspected where available.
- Manual check: select the new models in agent setup and editing.
Confirm Luna has Max but no Ultra, Grok 4.7 has Extra High, and Fable
5.1 has Extra High/Max. Save Grok effort and confirm
`adapterConfig.reasoningEffort` contains the selection.

## Risks

- Catalog presence does not grant account access. Older CLIs and
restricted accounts can reject a model. Opus 5.5 has an explicit upgrade
check.
- Higher effort can increase cost and latency. Existing agent defaults
are unchanged.
- Cursor fallback IDs come from public model documentation; the local
account exposed no live catalog. Runtime discovery still adds
account-specific variants.
- Kimi effort remains supported only on its explicit CLI engine. This
does not add effort support to its default ACP engine.
- Saved obsolete Bedrock or retired Gemini IDs are not migrated
automatically.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, tool use, and code
execution. The exact deployment ID and context window are not exposed to
this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin Foley
2026-09-22 16:56:00 -07:00
committed by GitHub
co-authored by Paperclip
parent 7badae6981
commit cdf04a33fa
29 changed files with 379 additions and 86 deletions
+81
View File
@@ -0,0 +1,81 @@
# Adapter model audit: September 22, 2026
This audit covers model selection in the existing coding-agent adapters. Catalog
entries identify models; provider accounts and installed CLIs determine access.
No agent defaults or saved model selections are migrated.
| Adapter | Changes from the audit |
| --- | --- |
| Claude Code | Add Opus 5.5 and require CLI 2.1.280. Fable 5.1, Fable 5, Sonnet 5, and Mythos 5 were already listed. Expose the documented model-specific effort levels in creation and editing. |
| Claude on Bedrock | Add Opus 5.5, Opus 5, Sonnet 5, Opus 4.7, and Sonnet 4.6. Correct the obsolete `-v1` suffix on Opus 4.8 and Fable 5. Apply the Opus 5.5 version check to Bedrock IDs too. |
| Codex and the Codex runner catalog | Add GPT-6 Sol and Luna, including Fast mode. Astra was already listed. Expose efforts through Ultra for Astra, Sol, and GPT-5.6 Sol/Terra; cap both Luna generations at Max. |
| Grok Build | Add Grok 4.7, 4.6, and 4.5. Offer Extra High for 4.7/4.6. Save edited effort as `reasoningEffort`, which the runtime consumes. Keep `grok-build` as the sentinel that lets the CLI choose its default. |
| Gemini CLI | Add Flash 3.8, 3.7, 3.6, 3.5, Flash Lite 3.5/3.1, and 3 Flash Preview. Remove the retired Gemini 2.0 choices. Keep Auto and the existing 3.1 Pro and 2.5 choices. |
| Cursor | Add the current documented fallback IDs for Composer 2.5, Opus 5.5, Fable 5.1, Sonnet 5, GPT-5.6 Sol/Terra/Luna, Gemini 3.8 Flash, Muse Spark 1.3, and Grok 4.7/4.6/4.5. Runtime model discovery remains available. |
| OpenCode | Refresh the static fallback used by remote environments with GPT-6 and GPT-5.6 families, current Claude models, Gemini 3.8 Flash, and Grok 4.7. |
| Kimi Code | Add K3 256K. Relabel `kimi-for-coding` as K2.8 Preview, which replaced K2.7 under the same ID. Forward CLI effort for K2.8 Preview and both K3 variants. |
## Sources and verification
- [Claude Code model configuration](https://code.claude.com/docs/en/model-config)
documents the Opus 5.5 CLI requirement and supported efforts.
[Opus 5.5 specifications](https://platform.claude.com/docs/en/models/opus-5-5/overview)
and [model ID conventions](https://platform.claude.com/docs/en/about-claude/models/model-ids-and-versions)
supply direct and Bedrock IDs. Bedrock entries use the catalog's existing US
inference-profile convention; regional availability remains account-dependent.
- [OpenAI's Codex model guide](https://learn.chatgpt.com/docs/models) lists GPT-6
Sol and Luna. The installed Codex model metadata independently lists Astra,
Sol, Luna, and all three GPT-5.6 variants with the effort sets used here.
[GPT-6 Luna specifications](https://developers.openai.com/api/docs/models/gpt-6-luna)
also document Fast pricing. Codex's Ultra setting is a CLI capability; it is
not inferred from the API's effort enum.
- [Grok 4.7](https://docs.x.ai/developers/grok-4-7) and
[Grok 4.6](https://docs.x.ai/developers/models/grok-4.6) document their IDs and
four effort levels. The installed `grok models` command had no authenticated
account and returned only its default sentinel. No inference was run.
- [Google's model catalog](https://ai.google.dev/gemini-api/docs/models) supplies
the new Gemini IDs. [The retirement schedule](https://ai.google.dev/gemini-api/docs/deprecations)
records Gemini 2.0 shutdown on June 1, 2026. Gemini 2.5 remains available to
existing users. [Gemini CLI model selection](https://geminicli.com/docs/cli/model/)
accepts an explicit model; login method and entitlement determine availability.
- [Cursor's catalog](https://cursor.com/docs/models-and-pricing) links each
model's exact ID. `cursor-agent --list-models` returned no models for the local
account, so the additions use documented IDs rather than guessed variant
suffixes. Account-specific Fast/thinking variants remain discoverable.
- [Kimi's model configuration](https://www.kimi.com/code/docs/en/kimi-code/models.html)
documents all four current IDs, K2.8's in-place alias upgrade, and effort
support. Kimi effort remains limited to the existing explicit CLI engine;
the default ACP engine's effort mapping is outside this catalog update.
## Other adapters and restricted models
OpenCode discovers local models, but remote environment routes use its static
fallback catalog. All twelve added provider-qualified IDs were also present in
the installed OpenCode registry. [OpenCode model configuration](https://opencode.ai/docs/models/)
documents its `provider/model` format and provider registry.
Pi already discovers models from its runtime or provider registry.
Hermes and OpenClaw accept provider configuration without a curated model list.
Cursor Cloud obtains its account's model list from Cursor. These paths do not
need a static entry per upstream release. Local OpenCode discovery returned
current models, including Muse Spark 1.3 and Nemotron 3.5 Lightning.
Anthropic describes Mythos 5.1 as invitation-only in the
[Fable 5.1 documentation](https://platform.claude.com/docs/en/models/fable-5-1/overview).
The public current-model catalog does not publish a selectable ID for it. Keep
account discovery and custom IDs available instead of inventing an ID. Grok
4.7 Fast is available in Grok Build and Cursor, but its exact account-specific
CLI variant ID was not exposed locally. It is not a public xAI API model.
Image, video, audio, and embedding models are outside these coding-agent pickers.
## Prior work checked
Fable 5.1 was already merged in [#12730](https://github.com/paperclipai/paperclip/pull/12730).
Astra was already merged in [#12851](https://github.com/paperclipai/paperclip/pull/12851).
The Grok 4.6/4.5 proposal [#11324](https://github.com/paperclipai/paperclip/pull/11324)
was closed and parked by its author. This update preserves the newer CLI-default
sentinel behavior from [#12062](https://github.com/paperclipai/paperclip/pull/12062).
Open model-discovery work such as [#13127](https://github.com/paperclipai/paperclip/pull/13127)
and [#13565](https://github.com/paperclipai/paperclip/pull/13565) is separate from
these catalog and effort corrections. No open PR covering the newly added IDs
was found before implementation.
@@ -1,7 +1,21 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_CLAUDE_LOCAL_MODEL, resolveClaudeModel } from "./index.js";
import { claudeLocalReasoningEffortsForModel, DEFAULT_CLAUDE_LOCAL_MODEL, resolveClaudeModel } from "./index.js";
import { minimumClaudeCliVersionForModel } from "./server/cli-capabilities.js";
describe("Claude model defaults", () => {
it.each(["claude-opus-5-5", "us.anthropic.claude-opus-5-5", "global.anthropic.claude-opus-5-5[1m]"])("requires a current CLI and offers all efforts for %s", (model) => {
expect(minimumClaudeCliVersionForModel(model)).toBe("2.1.280");
expect(claudeLocalReasoningEffortsForModel(model)).toEqual(["low", "medium", "high", "xhigh", "max"]);
});
it("keeps model-specific Claude reasoning limits", () => {
expect(claudeLocalReasoningEffortsForModel("claude-fable-5-1")).toContain("xhigh");
expect(claudeLocalReasoningEffortsForModel("claude-sonnet-5")).toContain("max");
expect(claudeLocalReasoningEffortsForModel("claude-sonnet-4-6")).toEqual(["low", "medium", "high", "max"]);
expect(claudeLocalReasoningEffortsForModel("claude-haiku-4-5")).toEqual([]);
expect(claudeLocalReasoningEffortsForModel("custom-model")).toEqual(["low", "medium", "high"]);
});
it.each([undefined, null, "", " "])("uses Opus 5 for an unset model (%j)", (model) => {
expect(DEFAULT_CLAUDE_LOCAL_MODEL).toBe("claude-opus-5");
expect(resolveClaudeModel(model)).toBe("claude-opus-5");
+13 -1
View File
@@ -22,6 +22,16 @@ export function resolveClaudeModel(
}
export const type = "claude_local";
export function claudeLocalReasoningEffortsForModel(model: string): readonly string[] {
const id = model.trim().replace(/\[1m\]$/, "").replace(/^(?:(?:us|eu|apac|global)\.)?anthropic\./, "");
if (/^claude-haiku-/.test(id)) return [];
if (/^claude-(?:opus-5(?:-5)?|opus-4-[78]|sonnet-5|fable-5(?:-1)?)$/.test(id)) {
return ["low", "medium", "high", "xhigh", "max"];
}
if (/^claude-(?:opus|sonnet)-4-6(?:-v1)?$/.test(id)) return ["low", "medium", "high", "max"];
return ["low", "medium", "high"];
}
export const label = "Claude Code";
export const SANDBOX_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code";
@@ -32,6 +42,7 @@ export const models = [
{ id: "claude-fable-5-1", label: "Claude Fable 5.1" },
{ id: "claude-fable-5", label: "Claude Fable 5" },
{ id: "claude-mythos-5", label: "Claude Mythos 5" },
{ id: "claude-opus-5-5", label: "Claude Opus 5.5" },
{ id: "claude-opus-5", label: "Claude Opus 5" },
{ id: "claude-opus-4-7", label: "Claude Opus 4.7" },
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
@@ -49,7 +60,7 @@ Core fields:
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
- instructionsFilePath (string, optional): absolute path to a markdown instructions file injected at runtime
- model (string, optional): Claude model id. Missing or blank defaults to ${DEFAULT_CLAUDE_LOCAL_MODEL} in both CLI and ACP, including existing agents. Explicit model IDs and ANTHROPIC_MODEL overrides are preserved. Bedrock/Vertex without an explicit model retain their provider default.
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
- effort (string, optional): model-specific reasoning effort passed via --effort (low|medium|high; current Opus, Sonnet 5, and Fable models also support xhigh|max)
- chrome (boolean, optional): pass --chrome when running Claude
- promptTemplate (string, optional): run prompt template
- maxTurnsPerRun (number, optional): max turns for one run
@@ -77,6 +88,7 @@ Operational fields:
- graceSec (number, optional): SIGTERM grace period in seconds
Notes:
- Claude Opus 5.5 uses model ID \`claude-opus-5-5\` and requires Claude Code v2.1.280 or later.
- filesystemScope and networkScope are spawn-level confinement and are orthogonal to Claude permission flags. Both require Bubblewrap on the host and explicit engine="cli"; default or explicit ACP is rejected because ACP confinement is not yet supported. networkScope="allowlist" injects HTTP_PROXY/HTTPS_PROXY for the CLI while its private network namespace blocks direct sockets, so every required provider/API hostname must be listed explicitly.
- The Claude ACP lane requires Node >=24.11.0 and @agentclientprotocol/claude-agent-acp to be installed with this adapter package. Missing prerequisites fail both default and explicit ACP runs with an actionable setup error; the adapter never switches engines automatically.
- For ACP runs, model selection is passed through ANTHROPIC_MODEL at ACP server startup; Paperclip-managed Claude permissions and ephemeral skill materialization are handled by the shared ACP engine.
@@ -6,11 +6,6 @@ const effortFlagSupportCache = new Map<string, Promise<boolean | null>>();
export const CLAUDE_FABLE_5_1_MIN_CLI_VERSION = "2.1.251";
const CLAUDE_FABLE_5_1_MODEL_IDS = new Set([
"claude-fable-5-1",
"us.anthropic.claude-fable-5-1",
]);
export function claudeCommandLooksLike(command: string, expected = "claude"): boolean {
const base = path.basename(command).toLowerCase();
return base === expected || base === `${expected}.cmd` || base === `${expected}.exe`;
@@ -41,7 +36,9 @@ function cacheKeyForTarget(command: string, target: AdapterExecutionTarget | nul
}
export function minimumClaudeCliVersionForModel(model: string): string | null {
return CLAUDE_FABLE_5_1_MODEL_IDS.has(model.trim())
const modelId = model.trim().replace(/\[1m\]$/, "").replace(/^(?:(?:us|eu|apac|global)\.)?anthropic\./, "");
if (modelId === "claude-opus-5-5") return "2.1.280";
return modelId === "claude-fable-5-1"
? CLAUDE_FABLE_5_1_MIN_CLI_VERSION
: null;
}
@@ -18,7 +18,7 @@ const {
signal: null,
timedOut: false,
stdout: args.includes("--version")
? "2.1.251 (Claude Code)\n"
? "2.1.280 (Claude Code)\n"
: [
JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-1", model: "claude-sonnet" }),
JSON.stringify({ type: "assistant", session_id: "claude-session-1", message: { content: [{ type: "text", text: "hello" }] } }),
@@ -466,14 +466,14 @@ describe("claude remote execution", () => {
return { args: call?.[2] ?? [], result };
}
it("passes the exact configured Fable 5.1 ID as --model on the CLI lane", async () => {
it.each(["claude-fable-5-1", "claude-opus-5-5"])("passes %s as --model on the CLI lane", async (model) => {
const { args } = await executeWithModel("paperclip-claude-model-direct-", {
model: "claude-fable-5-1",
model,
});
const modelFlag = args.indexOf("--model");
expect(modelFlag).toBeGreaterThanOrEqual(0);
expect(args[modelFlag + 1]).toBe("claude-fable-5-1");
expect(args[modelFlag + 1]).toBe(model);
});
it("passes the Bedrock-native Fable 5.1 ID as --model under Bedrock auth", async () => {
@@ -496,27 +496,30 @@ describe("claude remote execution", () => {
expect(args).not.toContain("--model");
});
it("rejects Fable 5.1 before launch when the CLI is older than 2.1.251", async () => {
it.each([
["claude-fable-5-1", "2.1.251", "2.1.247"],
["claude-opus-5-5", "2.1.280", "2.1.279"],
])("rejects %s before launch below CLI %s", async (model, minimumVersion, detectedVersion) => {
runChildProcess.mockResolvedValueOnce({
exitCode: 0,
signal: null,
timedOut: false,
stdout: "2.1.247 (Claude Code)\n",
stdout: `${detectedVersion} (Claude Code)\n`,
stderr: "",
pid: 123,
startedAt: new Date().toISOString(),
});
const { args, result } = await executeWithModel("paperclip-claude-model-old-cli-", {
model: "claude-fable-5-1",
model,
});
expect(args).toEqual([]);
expect(result.errorCode).toBe("claude_cli_version_incompatible");
expect(result.errorMessage).toContain("requires Claude Code 2.1.251 or newer");
expect(result.errorMessage).toContain(`${model} requires Claude Code ${minimumVersion} or newer`);
expect(result.resultJson).toMatchObject({
requiredClaudeCodeVersion: "2.1.251",
detectedClaudeCodeVersion: "2.1.247",
requiredClaudeCodeVersion: minimumVersion,
detectedClaudeCodeVersion: detectedVersion,
});
});
@@ -1286,7 +1286,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
? `detected ${detectedCliVersion}`
: "could not determine the installed version";
const errorMessage =
`Claude Fable 5.1 requires Claude Code ${minimumCliVersion} or newer on the CLI lane; ${detected}. ` +
`${model} requires Claude Code ${minimumCliVersion} or newer on the CLI lane; ${detected}. ` +
"Upgrade Claude Code or restore the default ACP lane before retrying.";
await onLog("stderr", `[paperclip] ${errorMessage}\n`);
return {
@@ -9,10 +9,15 @@ const ANTHROPIC_API_VERSION = "2023-06-01";
/** AWS Bedrock model IDs — region-qualified identifiers required by the Bedrock API. */
const BEDROCK_MODELS: AdapterModel[] = [
{ id: "us.anthropic.claude-opus-4-8-v1", label: "Bedrock Opus 4.8" },
{ id: "us.anthropic.claude-opus-4-8", label: "Bedrock Opus 4.8" },
{ id: "us.anthropic.claude-opus-5-5", label: "Bedrock Opus 5.5" },
{ id: "us.anthropic.claude-opus-5", label: "Bedrock Opus 5" },
{ id: "us.anthropic.claude-sonnet-5", label: "Bedrock Sonnet 5" },
// Fable 5.1's documented geo inference ID carries no -v1 suffix, unlike earlier entries.
{ id: "us.anthropic.claude-fable-5-1", label: "Bedrock Fable 5.1" },
{ id: "us.anthropic.claude-fable-5-v1", label: "Bedrock Fable 5" },
{ id: "us.anthropic.claude-fable-5", label: "Bedrock Fable 5" },
{ id: "us.anthropic.claude-opus-4-7", label: "Bedrock Opus 4.7" },
{ id: "us.anthropic.claude-sonnet-4-6", label: "Bedrock Sonnet 4.6" },
{ id: "us.anthropic.claude-opus-4-6-v1", label: "Bedrock Opus 4.6" },
{ id: "us.anthropic.claude-sonnet-4-5-20250929-v2:0", label: "Bedrock Sonnet 4.5" },
{ id: "us.anthropic.claude-haiku-4-5-20251001-v1:0", label: "Bedrock Haiku 4.5" },
@@ -639,14 +639,17 @@ describe("claude CLI local hello probe hardening", () => {
expect(JSON.stringify(spawnedEnv)).not.toContain("caller-proxy");
});
it("warns without executing when runtime PATH selects a different local Claude executable", async () => {
it.each([
["claude-fable-5-1", "2.1.251"],
["claude-opus-5-5", "2.1.280"],
])("warns without executing %s when runtime PATH selects a different executable", async (model, minimumVersion) => {
const runtimeDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-cli-runtime-path-"));
const runtimeClaudePath = path.join(runtimeDir, "claude");
await writeFile(runtimeClaudePath, "#!/bin/sh\nexit 0\n");
await chmod(runtimeClaudePath, 0o755);
try {
probeResult.value = { exitCode: 0, stdout: "2.1.251 (Claude Code)\n", stderr: "" };
probeResult.value = { exitCode: 0, stdout: `${minimumVersion} (Claude Code)\n`, stderr: "" };
const result = await testEnvironment({
companyId: "company-1",
@@ -654,7 +657,7 @@ describe("claude CLI local hello probe hardening", () => {
config: {
engine: "cli",
command: "claude",
model: "claude-fable-5-1",
model,
env: { PATH: runtimeDir },
},
executionTarget: null,
@@ -664,6 +667,7 @@ describe("claude CLI local hello probe hardening", () => {
expect(result.status).toBe("warn");
expect(result.checks).toContainEqual(expect.objectContaining({
code: "claude_cli_version_probe_mismatch",
hint: `Ensure the runtime-selected Claude Code is ${minimumVersion} or newer. Execution will verify that exact executable before launch.`,
level: "warn",
}));
expect(runAdapterExecutionTargetProcess).not.toHaveBeenCalled();
@@ -141,10 +141,13 @@ describe("claude sandbox auth-missing check", () => {
});
describe("claude CLI model compatibility check", () => {
it("fails before the hello probe when Fable 5.1 is configured with an older CLI", async () => {
it.each([
["claude-fable-5-1", "2.1.251", "2.1.247"],
["claude-opus-5-5", "2.1.280", "2.1.279"],
])("fails before the hello probe for %s below CLI %s", async (model, minimumVersion, detectedVersion) => {
probeResult.value = {
exitCode: 0,
stdout: "2.1.247 (Claude Code)\n",
stdout: `${detectedVersion} (Claude Code)\n`,
stderr: "",
};
@@ -154,7 +157,7 @@ describe("claude CLI model compatibility check", () => {
config: {
engine: "cli",
command: "claude",
model: "claude-fable-5-1",
model,
},
executionTarget: sandboxTarget,
environmentName: "Daytona",
@@ -164,7 +167,8 @@ describe("claude CLI model compatibility check", () => {
expect(result.checks).toContainEqual(expect.objectContaining({
code: "claude_cli_version_incompatible",
level: "error",
detail: "Detected Claude Code 2.1.247.",
message: `${model} requires Claude Code ${minimumVersion} or newer on the CLI lane.`,
detail: `Detected Claude Code ${detectedVersion}.`,
}));
expect(runAdapterExecutionTargetProcess).toHaveBeenCalledTimes(1);
const versionCall = runAdapterExecutionTargetProcess.mock.calls[0] as unknown as [
@@ -266,9 +266,9 @@ export async function testEnvironment(
code: "claude_cli_version_probe_mismatch",
level: "warn",
message:
"Skipped Fable 5.1 readiness probing because the runtime PATH selects a different Claude executable than the trusted local Test probe.",
`Skipped ${configuredModel} readiness probing because the runtime PATH selects a different Claude executable than the trusted local Test probe.`,
hint:
"Ensure the runtime-selected Claude Code is 2.1.251 or newer. Execution will verify that exact executable before launch.",
`Ensure the runtime-selected Claude Code is ${minimumCliVersion} or newer. Execution will verify that exact executable before launch.`,
});
} else if (canRunProbe && minimumCliVersion && versionProbeCommand) {
const versionProbeEnv = localProbe?.env ?? env;
@@ -289,7 +289,7 @@ export async function testEnvironment(
checks.push({
code: "claude_cli_version_incompatible",
level: "error",
message: `Claude Fable 5.1 requires Claude Code ${minimumCliVersion} or newer on the CLI lane.`,
message: `${configuredModel} requires Claude Code ${minimumCliVersion} or newer on the CLI lane.`,
detail: detectedCliVersion
? `Detected Claude Code ${detectedCliVersion}.`
: "Could not determine the installed Claude Code version.",
@@ -14,21 +14,24 @@ describe("codex local adapter metadata", () => {
// Default to the concrete gpt-5.6-sol slug — Codex ships no metadata for the bare gpt-5.6
// alias, so it must not be advertised or used as the default (it triggers a fallback warning).
expect(DEFAULT_CODEX_LOCAL_MODEL).toBe("gpt-5.6-sol");
expect(modelIds.slice(0, 4)).toEqual([
expect(modelIds.slice(0, 6)).toEqual([
"gpt-5.6-sol",
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-5.6-terra",
"gpt-5.6-luna",
]);
expect(modelIds).not.toContain("gpt-5.6");
expect(isCodexLocalFastModeSupported(DEFAULT_CODEX_LOCAL_MODEL)).toBe(true);
expect(isCodexLocalFastModeSupported("gpt-6-astra")).toBe(true);
expect(isCodexLocalFastModeSupported("gpt-6-sol")).toBe(true);
expect(modelIds).not.toContain("gpt-5.3-codex");
expect(modelIds).not.toContain("gpt-5.3-codex-spark");
});
it("uses the reasoning efforts advertised for GPT-6 Astra", () => {
expect(codexLocalReasoningEffortsForModel("gpt-6-astra")).toEqual([
it.each(["gpt-6-astra", "gpt-6-sol", " gpt-6-sol ", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6"])("uses the reasoning efforts advertised for %s", (model) => {
expect(codexLocalReasoningEffortsForModel(model)).toEqual([
"low",
"medium",
"high",
@@ -36,7 +39,10 @@ describe("codex local adapter metadata", () => {
"max",
"ultra",
]);
expect(codexLocalReasoningEffortsForModel("gpt-5.6-sol")).toEqual([
});
it.each(["gpt-5.5", "custom-model"])("preserves legacy efforts for %s", (model) => {
expect(codexLocalReasoningEffortsForModel(model)).toEqual([
"minimal",
"low",
"medium",
@@ -45,6 +51,11 @@ describe("codex local adapter metadata", () => {
]);
});
it.each(["gpt-6-luna", "gpt-5.6-luna"])("caps %s at max and supports Fast mode", (model) => {
expect(codexLocalReasoningEffortsForModel(model)).toEqual(["low", "medium", "high", "xhigh", "max"]);
expect(isCodexLocalFastModeSupported(model)).toBe(true);
});
it("normalizes the legacy bare gpt-5.6 alias to the concrete gpt-5.6-sol slug", () => {
expect(normalizeCodexModel("gpt-5.6")).toBe("gpt-5.6-sol");
expect(normalizeCodexModel(" gpt-5.6 ")).toBe("gpt-5.6-sol");
+26 -8
View File
@@ -14,6 +14,8 @@ export const DEFAULT_CODEX_LOCAL_MODEL = PAPERCLIP_RUNNER_DEFAULT_MODELS.codex;
export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true;
export const CODEX_LOCAL_FAST_MODE_SUPPORTED_MODELS = [
"gpt-6-astra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
@@ -41,18 +43,22 @@ const CODEX_LOCAL_DEFAULT_REASONING_EFFORTS = [
"xhigh",
] as const;
const CODEX_LOCAL_ASTRA_REASONING_EFFORTS = [
const CODEX_LOCAL_MAX_REASONING_EFFORTS = [
"low",
"medium",
"high",
"xhigh",
"max",
] as const;
const CODEX_LOCAL_ULTRA_REASONING_EFFORTS = [
...CODEX_LOCAL_MAX_REASONING_EFFORTS,
"ultra",
] as const;
export type CodexLocalReasoningEffort =
| (typeof CODEX_LOCAL_DEFAULT_REASONING_EFFORTS)[number]
| (typeof CODEX_LOCAL_ASTRA_REASONING_EFFORTS)[number];
| (typeof CODEX_LOCAL_ULTRA_REASONING_EFFORTS)[number];
export function normalizeCodexModel(model: string | null | undefined): string {
const normalizedModel = normalizeModelId(model);
@@ -62,9 +68,19 @@ export function normalizeCodexModel(model: string | null | undefined): string {
export function codexLocalReasoningEffortsForModel(
model: string | null | undefined,
): readonly CodexLocalReasoningEffort[] {
return normalizeCodexModel(model) === "gpt-6-astra"
? CODEX_LOCAL_ASTRA_REASONING_EFFORTS
: CODEX_LOCAL_DEFAULT_REASONING_EFFORTS;
const normalizedModel = normalizeCodexModel(model);
switch (normalizedModel) {
case "gpt-6-astra":
case "gpt-6-sol":
case "gpt-5.6-sol":
case "gpt-5.6-terra":
return CODEX_LOCAL_ULTRA_REASONING_EFFORTS;
case "gpt-6-luna":
case "gpt-5.6-luna":
return CODEX_LOCAL_MAX_REASONING_EFFORTS;
default:
return CODEX_LOCAL_DEFAULT_REASONING_EFFORTS;
}
}
export function isCodexLocalKnownModel(model: string | null | undefined): boolean {
@@ -94,6 +110,8 @@ export const models = [
// DEFAULT_CODEX_LOCAL_MODEL is gpt-5.6-sol, so it doubles as the first (default) 5.6 entry.
{ id: DEFAULT_CODEX_LOCAL_MODEL, label: DEFAULT_CODEX_LOCAL_MODEL },
{ id: "gpt-6-astra", label: "gpt-6-astra" },
{ id: "gpt-6-sol", label: "gpt-6-sol" },
{ id: "gpt-6-luna", label: "gpt-6-luna" },
{ id: "gpt-5.6-terra", label: "gpt-5.6-terra" },
{ id: "gpt-5.6-luna", label: "gpt-5.6-luna" },
{ id: "gpt-5.4", label: "gpt-5.4" },
@@ -116,10 +134,10 @@ Core fields:
- cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible)
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to stdin prompt at runtime
- model (string, optional): Codex model id
- modelReasoningEffort (string, optional): reasoning effort override passed via -c model_reasoning_effort=...; GPT-6 Astra supports low|medium|high|xhigh|max|ultra
- modelReasoningEffort (string, optional): reasoning effort override passed via -c model_reasoning_effort=...; GPT-6 Astra/Sol and GPT-5.6 Sol/Terra support low|medium|high|xhigh|max|ultra; GPT-6 Luna and GPT-5.6 Luna support low|medium|high|xhigh|max
- promptTemplate (string, optional): run prompt template
- search (boolean, optional): run codex with --search
- fastMode (boolean, optional): enable Codex Fast mode; supported on GPT-6 Astra, GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and passed through for manual model IDs
- fastMode (boolean, optional): enable Codex Fast mode; supported on GPT-6 (astra/sol/luna), GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and passed through for manual model IDs
- dangerouslyBypassApprovalsAndSandbox (boolean, optional): run with bypass flag
- command (string, optional): defaults to "codex"
- extraArgs (string[], optional): additional CLI args
@@ -150,7 +168,7 @@ Notes:
- Paperclip injects desired local skills into the effective CODEX_HOME/skills/ directory at execution time so Codex can discover "$paperclip" and related skills without polluting the project working directory. For new and updated agents, Paperclip assigns an isolated managed home at ~/.paperclip/instances/<id>/companies/<companyId>/agents/<agentId>/codex-home/skills/; when CODEX_HOME is explicitly overridden in adapter config, that override is used instead.
- New and updated codex_local agents persist an empty OPENAI_API_KEY override by default so a host-level OPENAI_API_KEY cannot leak into Codex runs through process inheritance. Explicit CODEX_HOME overrides must not point at the shared company codex-home, $CODEX_HOME, or ~/.codex.
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
- Fast mode is supported on GPT-6 Astra, GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`.
- Fast mode is supported on GPT-6 (astra/sol/luna), GPT-5.6 (sol/terra/luna), GPT-5.5, GPT-5.4 and manual model IDs. When enabled for those models, Paperclip applies \`service_tier="fast"\` and \`features.fast_mode=true\`.
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
- The ACP engine keeps its workspace sandbox and enables network access on each turn. Explicit sandbox_workspace_write.network_access overrides in extraArgs (or env.PAPERCLIP_CODEX_ACP_NETWORK_ACCESS="false") disable it; execution-target network denial wins. The bundled ACP patch is needed because upstream mode presets override Codex config.toml on every turn.
- The CLI engine defaults to a writable workspace sandbox with network access for unattended work and Paperclip API calls. It does not enable the dangerous bypass flag. Explicit sandbox modes/profiles and network overrides in extraArgs retain their meaning. An execution-target network denial remains enforced.
@@ -508,15 +508,15 @@ describe("codex_local ACP lane", () => {
});
});
it("forwards GPT-6 Astra controls to the ACPX Codex target", () => {
it.each([["gpt-6-astra", "ultra"], ["gpt-6-sol", "ultra"], ["gpt-6-luna", "max"], ["gpt-5.6-sol", "ultra"], ["gpt-5.6-terra", "ultra"], ["gpt-5.6-luna", "max"]])("forwards %s controls to the ACPX Codex target", (model, effort) => {
expect(buildCodexAcpConfig({
engine: "acp",
model: "gpt-6-astra",
modelReasoningEffort: "ultra",
model,
modelReasoningEffort: effort,
fastMode: true,
})).toMatchObject({
model: "gpt-6-astra",
modelReasoningEffort: "ultra",
model,
modelReasoningEffort: effort,
fastMode: true,
});
});
@@ -9,14 +9,14 @@ describe("buildCodexExecArgs", () => {
if (resumeSessionId) expect(args.slice(-3)).toEqual(["resume", resumeSessionId, "-"]);
});
it("forwards GPT-6 Astra, its ultra reasoning effort, and fast mode", () => {
it.each([["gpt-6-astra", "ultra"], ["gpt-6-sol", "ultra"], ["gpt-6-luna", "max"], ["gpt-5.6-sol", "ultra"], ["gpt-5.6-terra", "ultra"], ["gpt-5.6-luna", "max"]])("forwards %s, its supported reasoning effort, and fast mode", (model, effort) => {
const result = buildCodexExecArgs({
model: "gpt-6-astra",
modelReasoningEffort: "ultra",
model,
modelReasoningEffort: effort,
fastMode: true,
});
expect(result.model).toBe("gpt-6-astra");
expect(result.model).toBe(model);
expect(result.fastModeApplied).toBe(true);
expect(result.fastModeIgnoredReason).toBeNull();
expect(result.args).toEqual([
@@ -24,9 +24,9 @@ describe("buildCodexExecArgs", () => {
"--json",
"--dangerously-bypass-approvals-and-sandbox",
"--model",
"gpt-6-astra",
model,
"-c",
'model_reasoning_effort="ultra"',
`model_reasoning_effort="${effort}"`,
"-c",
'service_tier="fast"',
"-c",
@@ -148,7 +148,7 @@ describe("buildCodexExecArgs", () => {
expect(result.fastModeRequested).toBe(true);
expect(result.fastModeApplied).toBe(false);
expect(result.fastModeIgnoredReason).toContain(
"currently only supported on gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4 or manually configured model IDs",
"currently only supported on gpt-6-astra, gpt-6-sol, gpt-6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4 or manually configured model IDs",
);
expect(result.args).toEqual([
"exec",
@@ -63,18 +63,18 @@ describe("buildCodexLocalConfig", () => {
});
});
it("persists the exact GPT-6 Astra model and supported controls", () => {
it.each([["gpt-6-astra", "ultra"], ["gpt-6-sol", "ultra"], ["gpt-6-luna", "max"], ["gpt-5.6-sol", "ultra"], ["gpt-5.6-terra", "ultra"], ["gpt-5.6-luna", "max"]])("persists the exact %s model and supported controls", (model, effort) => {
const config = buildCodexLocalConfig(
makeValues({
model: "gpt-6-astra",
thinkingEffort: "ultra",
model,
thinkingEffort: effort,
fastMode: true,
}),
);
expect(config).toMatchObject({
model: "gpt-6-astra",
modelReasoningEffort: "ultra",
model,
modelReasoningEffort: effort,
fastMode: true,
});
});
@@ -14,6 +14,18 @@ export const DEFAULT_CURSOR_LOCAL_MODEL = "auto";
const CURSOR_FALLBACK_MODEL_IDS = [
"auto",
"composer-2.5",
"claude-opus-5-5",
"claude-fable-5-1",
"claude-sonnet-5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gemini-3.8-flash",
"muse-spark-1.3",
"grok-4.7",
"grok-4.6",
"grok-4.5",
"composer-1.5",
"composer-1",
"gpt-5.3-codex-low",
+7 -2
View File
@@ -13,11 +13,16 @@ export const models = [
{ id: DEFAULT_GEMINI_LOCAL_MODEL, label: "Auto" },
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro Preview" },
{ id: "gemini-3.1-pro-preview-customtools", label: "Gemini 3.1 Pro Preview (Custom Tools)" },
{ id: "gemini-3.8-flash", label: "Gemini 3.8 Flash" },
{ id: "gemini-3.7-flash", label: "Gemini 3.7 Flash" },
{ id: "gemini-3.6-flash", label: "Gemini 3.6 Flash" },
{ id: "gemini-3.5-flash", label: "Gemini 3.5 Flash" },
{ id: "gemini-3.5-flash-lite", label: "Gemini 3.5 Flash Lite" },
{ id: "gemini-3.1-flash-lite", label: "Gemini 3.1 Flash Lite" },
{ id: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview" },
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
{ id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
{ id: "gemini-2.0-flash", label: "Gemini 2.0 Flash" },
{ id: "gemini-2.0-flash-lite", label: "Gemini 2.0 Flash Lite" },
];
export const agentConfigurationDoc = `# gemini_local agent configuration
+10 -1
View File
@@ -5,8 +5,17 @@ export const DEFAULT_GROK_LOCAL_MODEL = "grok-build";
export const models = [
{ id: DEFAULT_GROK_LOCAL_MODEL, label: DEFAULT_GROK_LOCAL_MODEL },
{ id: "grok-4.7", label: "Grok 4.7" },
{ id: "grok-4.6", label: "Grok 4.6" },
{ id: "grok-4.5", label: "Grok 4.5" },
];
export function grokLocalReasoningEffortsForModel(model: string): readonly string[] {
return model.trim() === "grok-4.7" || model.trim() === "grok-4.6"
? ["low", "medium", "high", "xhigh"]
: ["low", "medium", "high"];
}
export const agentConfigurationDoc = `# grok_local agent configuration
Adapter: grok_local
@@ -27,7 +36,7 @@ Core fields:
- promptTemplate (string, optional): run prompt template
- model (string, optional): Grok model id. Defaults to grok-build.
- permissionMode (string, optional): Grok permission mode passed via \`--permission-mode\`. Unset by default: Grok >= 1.0 enforces \`dontAsk\` as deny-by-default and it overrides \`--always-approve\`, so unattended runs rely on \`--always-approve\` alone unless you explicitly need a mode
- reasoningEffort (string, optional): Grok reasoning effort passed via \`--reasoning-effort\`
- reasoningEffort (string, optional): Grok reasoning effort (low|medium|high; grok-4.7 and grok-4.6 also accept xhigh) passed via \`--reasoning-effort\`
- maxTurns (number, optional): maximum agent turns for the run
- command (string, optional): defaults to "grok"
- extraArgs (string[], optional): additional CLI args
@@ -160,6 +160,19 @@ async function makeCtx(runId: string, cwd: string): Promise<AdapterExecutionCont
}
describe("grok_local execute", () => {
it.each(["grok-4.7", "grok-4.6"])("forwards the explicit %s model and xhigh effort", async (model) => {
const root = await makeTempRoot();
const ctx = await makeCtx("model-selection", root);
ctx.config = { cwd: root, model, reasoningEffort: "xhigh" };
runProcessMock.mockResolvedValue(makeSuccessfulRunResult());
await execute(ctx);
const args = runProcessMock.mock.calls[0][3] as string[];
expect(args[args.indexOf("--model") + 1]).toBe(model);
expect(args[args.indexOf("--reasoning-effort") + 1]).toBe("xhigh");
});
beforeEach(() => {
mocks.state.isRemote = false;
mocks.state.prepareRuntimeResult = null;
+8 -3
View File
@@ -8,9 +8,10 @@ export const SANDBOX_INSTALL_COMMAND = buildSandboxNpmInstallCommand("@moonshot-
export const DEFAULT_KIMI_LOCAL_MODEL = "kimi-code/kimi-for-coding";
export const models = [
{ id: DEFAULT_KIMI_LOCAL_MODEL, label: "K2.7 Coding" },
{ id: DEFAULT_KIMI_LOCAL_MODEL, label: "K2.8 Preview" },
{ id: "kimi-code/kimi-for-coding-highspeed", label: "K2.7 Coding Highspeed" },
{ id: "kimi-code/k3", label: "K3" },
{ id: "kimi-code/k3-256k", label: "K3 (256K)" },
];
/**
@@ -26,7 +27,11 @@ export type KimiEffort = (typeof KIMI_SUPPORTED_EFFORTS)[number];
* Models that advertise `support_efforts` in Kimi's model catalog. Keep in
* sync with `models` above; only these accept KIMI_MODEL_THINKING_EFFORT.
*/
export const EFFORT_CAPABLE_MODELS = new Set<string>(["kimi-code/k3"]);
export const EFFORT_CAPABLE_MODELS = new Set<string>([
DEFAULT_KIMI_LOCAL_MODEL,
"kimi-code/k3",
"kimi-code/k3-256k",
]);
export function modelSupportsEffort(model: string): boolean {
return EFFORT_CAPABLE_MODELS.has(model.trim());
@@ -65,7 +70,7 @@ Core fields:
- instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt. Sibling files in the same directory (HEARTBEAT.md, SOUL.md, TOOLS.md) are made readable via --add-dir for local runs.
- promptTemplate (string, optional): run prompt template
- model (string, optional): Kimi model alias (provider/model). Defaults to kimi-code/kimi-for-coding.
- effort (string, optional): thinking effort (low | medium | high | max). CLI lane only (engine=cli): forwarded as KIMI_MODEL_THINKING_EFFORT for effort-capable models (currently kimi-code/k3); "medium" maps to "high" since Kimi has no medium tier. Ignored for models without support_efforts, and NOT forwarded on the default ACP engine lane (Kimi ACP exposes a separate "thinking" option that is not wired yet) — pin engine=cli when effort control matters.
- effort (string, optional): thinking effort (low | medium | high | max). CLI lane only (engine=cli): forwarded as KIMI_MODEL_THINKING_EFFORT for effort-capable models (K2.8 Preview, K3, and K3 256K); "medium" maps to "high" since Kimi has no medium tier. Ignored for models without support_efforts, and NOT forwarded on the default ACP engine lane (Kimi ACP exposes a separate "thinking" option that is not wired yet) — pin engine=cli when effort control matters.
- command (string, optional): defaults to "kimi"
- extraArgs (string[], optional): additional CLI args
- env (object, optional): KEY=VALUE environment variables
@@ -333,7 +333,7 @@ describe("kimi_local execute", () => {
expect(seenEnv.TERM).toBe("xterm-256color");
});
it("forwards configured effort as KIMI_MODEL_THINKING_EFFORT for effort-capable models", async () => {
it.each(["kimi-code/k3", "kimi-code/k3-256k", "kimi-code/kimi-for-coding"])("forwards configured effort for %s", async (model) => {
const root = await makeTempRoot();
let seenEnv: Record<string, string> = {};
runProcessMock.mockImplementation(async (_runId, _target, _command, _args, options) => {
@@ -341,7 +341,7 @@ describe("kimi_local execute", () => {
return { exitCode: 0, signal: null, timedOut: false, stdout: KIMI_STDOUT, stderr: "" };
});
await execute(makeContext(root, { config: { cwd: root, model: "kimi-code/k3", effort: "high" } }));
await execute(makeContext(root, { config: { cwd: root, model, effort: "high" } }));
expect(seenEnv.KIMI_MODEL_THINKING_EFFORT).toBe("high");
});
@@ -368,7 +368,7 @@ describe("kimi_local execute", () => {
});
await execute(makeContext(root, {
config: { cwd: root, model: "kimi-code/kimi-for-coding", effort: "high" },
config: { cwd: root, model: "kimi-code/kimi-for-coding-highspeed", effort: "high" },
}));
expect(seenEnv.KIMI_MODEL_THINKING_EFFORT).toBeUndefined();
@@ -53,6 +53,18 @@ export function isValidOpenCodeModelId(value: unknown): value is string {
export const models: Array<{ id: string; label: string }> = [
{ id: DEFAULT_OPENCODE_LOCAL_MODEL, label: DEFAULT_OPENCODE_LOCAL_MODEL },
{ id: "openai/gpt-6-astra", label: "openai/gpt-6-astra" },
{ id: "openai/gpt-6-sol", label: "openai/gpt-6-sol" },
{ id: "openai/gpt-6-luna", label: "openai/gpt-6-luna" },
{ id: "openai/gpt-5.6-sol", label: "openai/gpt-5.6-sol" },
{ id: "openai/gpt-5.6-terra", label: "openai/gpt-5.6-terra" },
{ id: "openai/gpt-5.6-luna", label: "openai/gpt-5.6-luna" },
{ id: "anthropic/claude-opus-5-5", label: "anthropic/claude-opus-5-5" },
{ id: "anthropic/claude-opus-5", label: "anthropic/claude-opus-5" },
{ id: "anthropic/claude-fable-5-1", label: "anthropic/claude-fable-5-1" },
{ id: "anthropic/claude-sonnet-5", label: "anthropic/claude-sonnet-5" },
{ id: "google/gemini-3.8-flash", label: "google/gemini-3.8-flash" },
{ id: "xai/grok-4.7", label: "xai/grok-4.7" },
{ id: "openai/gpt-5.5", label: "openai/gpt-5.5" },
{ id: "openai/gpt-5.4", label: "openai/gpt-5.4" },
{ id: "openai/gpt-5.4-mini", label: "openai/gpt-5.4-mini" },
+42 -7
View File
@@ -71,6 +71,7 @@ describe("adapter model listing", () => {
expect(models.some((model) => model.id === "claude-mythos-5")).toBe(true);
// Opus 5 is a current GA flagship and must be offered even when live discovery is unavailable.
expect(models.some((model) => model.id === "claude-opus-5")).toBe(true);
expect(models).toContainEqual({ id: "claude-opus-5-5", label: "Claude Opus 5.5" });
expect(fetchSpy).not.toHaveBeenCalled();
});
@@ -93,6 +94,7 @@ describe("adapter model listing", () => {
expect(first).toEqual(second);
expect(first.some((model) => model.id === "claude-opus-4-8-20260529")).toBe(true);
expect(first.some((model) => model.id === "claude-opus-4-8")).toBe(true);
expect(first.some((model) => model.id === "claude-opus-5-5")).toBe(true);
});
it("refreshes cached claude models on demand", async () => {
@@ -131,18 +133,21 @@ describe("adapter model listing", () => {
expect(models).toEqual(claudeFallbackModels);
});
it("does not duplicate claude-fable-5-1 when discovery returns the identical ID", async () => {
it.each([
["claude-fable-5-1", "Claude Fable 5.1"],
["claude-opus-5-5", "Claude Opus 5.5"],
])("does not duplicate %s when discovery returns the identical ID", async (id, displayName) => {
process.env.ANTHROPIC_API_KEY = "sk-ant-test";
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => ({
data: [{ id: "claude-fable-5-1", display_name: "Claude Fable 5.1" }],
data: [{ id, display_name: displayName }],
}),
} as Response);
const models = await listAdapterModels("claude_local");
expect(models.filter((model) => model.id === "claude-fable-5-1")).toHaveLength(1);
expect(models.filter((model) => model.id === id)).toEqual([{ id, label: displayName }]);
// Curated fallbacks discovery did not return are still merged in.
expect(models.some((model) => model.id === "claude-fable-5")).toBe(true);
expect(models.some((model) => model.id === "claude-opus-4-8")).toBe(true);
@@ -154,13 +159,42 @@ describe("adapter model listing", () => {
const models = await listAdapterModels("claude_local");
// The Bedrock default (first entry) is unchanged.
expect(models[0]?.id).toBe("us.anthropic.claude-opus-4-8-v1");
expect(models.some((model) => model.id === "us.anthropic.claude-fable-5-1")).toBe(true);
// Keep Opus 4.8 first, using its documented dateless Bedrock ID.
expect(models[0]?.id).toBe("us.anthropic.claude-opus-4-8");
expect(models.map((model) => model.id)).toEqual(expect.arrayContaining([
"us.anthropic.claude-opus-5-5", "us.anthropic.claude-opus-5", "us.anthropic.claude-sonnet-5",
"us.anthropic.claude-fable-5-1", "us.anthropic.claude-opus-4-7", "us.anthropic.claude-sonnet-4-6",
]));
expect(models.map((model) => model.id)).not.toEqual(expect.arrayContaining(["us.anthropic.claude-opus-4-8-v1"]));
expect(models.some((model) => model.id === "claude-fable-5-1")).toBe(false);
expect(fetchSpy).not.toHaveBeenCalled();
});
it.each([
["gemini_local", ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-flash-lite", "gemini-3-flash-preview"]],
["grok_local", ["grok-build", "grok-4.7", "grok-4.6", "grok-4.5"]],
["kimi_local", ["kimi-code/kimi-for-coding", "kimi-code/k3", "kimi-code/k3-256k"]],
])("lists current %s models without a provider login", async (adapter, expectedIds) => {
const models = await listAdapterModels(adapter as string);
expect(models.map((model) => model.id)).toEqual(expect.arrayContaining(expectedIds as string[]));
expect(new Set(models.map((model) => model.id)).size).toBe(models.length);
if (adapter === "gemini_local") {
expect(models.some((model) => model.id.startsWith("gemini-2.0-"))).toBe(false);
}
if (adapter === "kimi_local") {
expect(models).toContainEqual({ id: "kimi-code/kimi-for-coding", label: "K2.8 Preview" });
}
});
it("includes current Cursor fallbacks when runtime discovery is unavailable", async () => {
setCursorModelsRunnerForTests(() => ({ status: 1, stdout: "", stderr: "", hasError: true }));
const models = await listAdapterModels("cursor");
expect(models.map((model) => model.id)).toEqual(expect.arrayContaining([
"composer-2.5", "claude-opus-5-5", "claude-fable-5-1", "claude-sonnet-5",
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "grok-4.7", "gemini-3.8-flash", "muse-spark-1.3",
]));
});
it("loads codex models dynamically and merges fallback options", async () => {
process.env.OPENAI_API_KEY = "sk-test";
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue({
@@ -232,12 +266,13 @@ describe("adapter model listing", () => {
expect(models).toEqual(cursorFallbackModels);
});
it("returns opencode fallback models including gpt-5.4", async () => {
it("returns current provider-qualified OpenCode models when discovery is unavailable", async () => {
process.env.PAPERCLIP_OPENCODE_COMMAND = "__paperclip_missing_opencode_command__";
const models = await listAdapterModels("opencode_local");
expect(models).toEqual(opencodeFallbackModels);
expect(models.map((model) => model.id)).toEqual(expect.arrayContaining(["openai/gpt-6-astra", "openai/gpt-6-sol", "openai/gpt-6-luna", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "openai/gpt-5.6-luna", "anthropic/claude-opus-5-5", "anthropic/claude-opus-5", "anthropic/claude-fable-5-1", "anthropic/claude-sonnet-5", "google/gemini-3.8-flash", "xai/grok-4.7"]));
});
it("loads cursor models dynamically and caches them", async () => {
@@ -784,6 +784,23 @@ describe("AgentConfigForm environment selector", () => {
expect(result.onSave.mock.calls[0][0].adapterConfig.effort).toBeUndefined();
});
it("saves Grok 4.7 reasoning effort using the runtime key", async () => {
const result = await renderForm([], { adapterType: "grok_local", adapterConfig: { model: "grok-4.7", reasoningEffort: "high" } });
roots.push(result.root);
const effort = [...result.container.querySelectorAll("button")].find(button => button.textContent?.trim() === "High")!;
expect(effort).toBeTruthy();
await act(async () => effort.click());
await flushReact();
const xhigh = [...document.querySelectorAll("button")].find(button => button.textContent?.trim() === "X-Highxhigh")!;
expect(xhigh).toBeTruthy();
await act(async () => xhigh.click());
await flushReact();
const save = [...result.container.querySelectorAll("button")].find(button => button.textContent?.trim() === "Save")!;
await act(async () => save.click());
expect(result.onSave).toHaveBeenCalledWith(expect.objectContaining({ adapterConfig: expect.objectContaining({ reasoningEffort: "xhigh" }) }));
expect(result.onSave.mock.calls[0][0].adapterConfig.effort).toBeUndefined();
});
it("hides the environment override when Local is the only configured environment", async () => {
const result = await renderForm([
makeEnvironment({ id: "local-1", name: "Local", driver: "local" }),
+11 -4
View File
@@ -1,6 +1,7 @@
import { AiConnectionField } from "./ai-connections/AiConnectionField";
import { aiConnectionBindingSchema } from "@paperclipai/shared";
import { testAgentSetup } from "@/lib/test-agent-setup";
import { setupEfforts } from "../lib/agent-setup-fields";
import { RuntimeTestCard } from "./RuntimeTestCard";
import { useState, useEffect, useRef, useMemo, useCallback, Children, isValidElement, type ReactNode } from "react";
import type { AdapterConfigSection } from "../adapters/types";
@@ -1268,6 +1269,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
? "mode"
: adapterType === "opencode_local"
? "variant"
: adapterType === "grok_local" ? "reasoningEffort"
: adapterType === "pi_local" ? "thinking" : "effort";
const thinkingEffortOptions =
adapterType === "codex_local"
@@ -1283,7 +1285,12 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
? kimiThinkingEffortOptions
: adapterType === "pi_local"
? [{ id: "", label: "Auto" }, ...["off", "minimal", "low", "medium", "high", "xhigh"].map(id => ({ id, label: id }))]
: claudeThinkingEffortOptions;
: adapterType === "claude_local" || adapterType === "grok_local"
? [{ id: "", label: "Auto" }, ...setupEfforts(adapterType, currentModelId).map((id) => ({
id,
label: id === "xhigh" ? "X-High" : id[0].toUpperCase() + id.slice(1),
}))]
: claudeThinkingEffortOptions;
const currentThinkingEffort = isCreate
? val!.thinkingEffort
: adapterType === "codex_local"
@@ -1713,10 +1720,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
models={models}
value={currentModelId}
onChange={(v) => {
const supportedEfforts = codexReasoningEffortOptions(v, "Auto");
const clearUnsupportedEffort = adapterType === "codex_local"
const supportedEfforts = setupEfforts(adapterType, v);
const clearUnsupportedEffort = ["codex_local", "claude_local", "grok_local"].includes(adapterType)
&& Boolean(currentThinkingEffort)
&& !supportedEfforts.some((option) => option.value === currentThinkingEffort);
&& !supportedEfforts.includes(String(currentThinkingEffort));
if (isCreate) {
set!({
model: v,
@@ -34,7 +34,7 @@ export const help: Record<string, string> = {
dangerouslySkipPermissions: "Run unattended by auto-approving adapter permission prompts when supported.",
dangerouslyBypassSandbox: "Run Codex without sandbox restrictions. Required for filesystem/network access.",
search: "Enable Codex web search capability during runs.",
fastMode: "Enable Codex Fast mode. This burns credits/tokens much faster and is supported on GPT-6 Astra, GPT-5.6, GPT-5.5, GPT-5.4, and manual Codex model IDs.",
fastMode: "Enable Codex Fast mode. This burns credits/tokens much faster and is supported on GPT-6, GPT-5.6, GPT-5.5, GPT-5.4, and manual Codex model IDs.",
workspaceStrategy: "How Paperclip should realize an execution workspace for this agent. Keep project_primary for normal cwd execution, or use git_worktree for issue-scoped isolated checkouts.",
workspaceBaseRef: "Base git ref used when creating a worktree branch. Leave blank to use the resolved workspace ref or HEAD.",
workspaceBranchTemplate: "Template for naming derived branches. Supports {{issue.identifier}}, {{issue.title}}, {{agent.name}}, {{project.id}}, {{workspace.repoRef}}, and {{slug}}.",
+21
View File
@@ -0,0 +1,21 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { setupEfforts } from "./agent-setup-fields";
describe("model-specific setup efforts", () => {
it("offers current Claude efforts without offering them on Haiku", () => {
expect(setupEfforts("claude_local", "claude-fable-5-1")).toEqual(["low", "medium", "high", "xhigh", "max"]);
expect(setupEfforts("claude_local", "claude-haiku-4-5")).toEqual([]);
});
it("offers xhigh on Grok 4.7 and 4.6, with the lower limit on 4.5", () => {
expect(setupEfforts("grok_local", "grok-4.7")).toEqual(["low", "medium", "high", "xhigh"]);
expect(setupEfforts("grok_local", "grok-4.6")).toContain("xhigh");
expect(setupEfforts("grok_local", "grok-4.5")).toEqual(["low", "medium", "high"]);
});
it("caps Luna at max while exposing ultra on Sol", () => {
expect(setupEfforts("codex_local", "gpt-6-luna")).toEqual(["low", "medium", "high", "xhigh", "max"]);
expect(setupEfforts("codex_local", "gpt-6-sol")).toContain("ultra");
});
});
+4 -2
View File
@@ -1,4 +1,6 @@
import { DEFAULT_CODEX_LOCAL_MODEL } from "@paperclipai/adapter-codex-local";
import { claudeLocalReasoningEffortsForModel, DEFAULT_CLAUDE_LOCAL_MODEL } from "@paperclipai/adapter-claude-local";
import { grokLocalReasoningEffortsForModel } from "@paperclipai/adapter-grok-local";
import { codexReasoningEffortOptions } from "./codex-reasoning-effort";
import { PROVIDER_ENV_KEYS } from "./provider-credential";
@@ -27,7 +29,7 @@ export function setupProviderKeys(adapter: string) {
export function setupEfforts(adapter: string, model = ""): string[] {
switch (adapter) {
case "claude_local":
return ["low", "medium", "high"];
return [...claudeLocalReasoningEffortsForModel(model || DEFAULT_CLAUDE_LOCAL_MODEL)];
case "codex_local":
return codexReasoningEffortOptions(model || DEFAULT_CODEX_LOCAL_MODEL)
.map((option) => option.value)
@@ -35,7 +37,7 @@ export function setupEfforts(adapter: string, model = ""): string[] {
case "pi_local":
return ["off", "minimal", "low", "medium", "high", "xhigh"];
case "grok_local":
return ["low", "medium", "high"];
return [...grokLocalReasoningEffortsForModel(model)];
default:
return [];
}
+9 -3
View File
@@ -4,8 +4,8 @@ import { describe, expect, it } from "vitest";
import { codexReasoningEffortOptions } from "./codex-reasoning-effort";
describe("codexReasoningEffortOptions", () => {
it("exposes only the supported GPT-6 Astra reasoning efforts", () => {
expect(codexReasoningEffortOptions("gpt-6-astra")).toEqual([
it.each(["gpt-6-astra", "gpt-6-sol", "gpt-5.6-sol", "gpt-5.6-terra"])("exposes only the supported %s reasoning efforts", (model) => {
expect(codexReasoningEffortOptions(model)).toEqual([
{ value: "", label: "Default" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
@@ -16,8 +16,14 @@ describe("codexReasoningEffortOptions", () => {
]);
});
it.each(["gpt-6-luna", "gpt-5.6-luna"])("caps %s at Max", (model) => {
expect(codexReasoningEffortOptions(model).map((option) => option.value)).toEqual([
"", "low", "medium", "high", "xhigh", "max",
]);
});
it("preserves the existing choices for other and manual models", () => {
expect(codexReasoningEffortOptions("gpt-5.6-sol").map((option) => option.value)).toEqual([
expect(codexReasoningEffortOptions("custom-model").map((option) => option.value)).toEqual([
"",
"minimal",
"low",