v0.2.1: optional per-session model override (Cobb wants Opus on code-work paths)
- CreateSessionRequest gains optional `model` field (validated regex) - AcpxManager.create + AcpxSession dataclass carry the model - Subprocess env on both create and turn sets ANTHROPIC_MODEL=<model> when set; subprocesses inherit parent env unchanged when NULL - store.insert_session takes optional model; sessions table grows a nullable model column via idempotent ALTER TABLE in Store.__init__ so live deployments migrate on next boot - POST /sessions response echoes model so callers can confirm - session_events 'create' meta records the model Backward-compatible: omitting model preserves current Sonnet-default behavior. The 14 SDK Session APIs work unchanged; SDK updates can land opportunistically when callers need to pin a model. crafting-table's patcher pins 'opus' in a follow-up commit on the crafting-table side.
This commit is contained in:
parent
1f6606d3b9
commit
dbbead261d
3 changed files with 61 additions and 5 deletions
|
|
@ -39,6 +39,19 @@ ACPX_EXIT_NO_SESSION = 4
|
||||||
ACPX_EXIT_PERMISSION_DENIED = 5
|
ACPX_EXIT_PERMISSION_DENIED = 5
|
||||||
|
|
||||||
|
|
||||||
|
def _env_with_model(model: str | None) -> dict[str, str] | None:
|
||||||
|
"""Build subprocess env, optionally pinning ANTHROPIC_MODEL.
|
||||||
|
|
||||||
|
The underlying claude CLI honors ANTHROPIC_MODEL env to pick the model
|
||||||
|
for an invocation. acpx forwards env to the agent process, so setting
|
||||||
|
it here propagates through the chain. Returns None when no override
|
||||||
|
is needed so create_subprocess_exec inherits the parent env unchanged.
|
||||||
|
"""
|
||||||
|
if not model:
|
||||||
|
return None
|
||||||
|
return {**os.environ, "ANTHROPIC_MODEL": model}
|
||||||
|
|
||||||
|
|
||||||
class AcpxError(Exception):
|
class AcpxError(Exception):
|
||||||
"""Base class for acpx-runner failures surfaced to the API layer."""
|
"""Base class for acpx-runner failures surfaced to the API layer."""
|
||||||
|
|
||||||
|
|
@ -92,6 +105,7 @@ class AcpxSession:
|
||||||
agent: str
|
agent: str
|
||||||
cwd: Path
|
cwd: Path
|
||||||
created_at: int
|
created_at: int
|
||||||
|
model: str | None = None # optional per-session model override; propagated to subprocess env as ANTHROPIC_MODEL
|
||||||
closed: bool = False
|
closed: bool = False
|
||||||
last_turn_at: int | None = None
|
last_turn_at: int | None = None
|
||||||
turn_count: int = 0
|
turn_count: int = 0
|
||||||
|
|
@ -130,12 +144,23 @@ class AcpxManager:
|
||||||
|
|
||||||
# ---- public API ------------------------------------------------------
|
# ---- public API ------------------------------------------------------
|
||||||
|
|
||||||
async def create(self, *, app_name: str, agent: str = "claude") -> AcpxSession:
|
async def create(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
app_name: str,
|
||||||
|
agent: str = "claude",
|
||||||
|
model: str | None = None,
|
||||||
|
) -> AcpxSession:
|
||||||
"""Create a new acpx-backed session.
|
"""Create a new acpx-backed session.
|
||||||
|
|
||||||
Mints a UUID, makes a per-session cwd, runs `acpx <agent> sessions
|
Mints a UUID, makes a per-session cwd, runs `acpx <agent> sessions
|
||||||
new --name <uuid> --cwd <dir> --format json`, captures the
|
new --name <uuid> --cwd <dir> --format json`, captures the
|
||||||
session_ensured event, returns the in-memory handle.
|
session_ensured event, returns the in-memory handle.
|
||||||
|
|
||||||
|
`model`, when set, is propagated to the underlying claude CLI via
|
||||||
|
the ANTHROPIC_MODEL env var on both this create call and every
|
||||||
|
turn() against the resulting session. NULL = let the CLI default
|
||||||
|
decide (Sonnet at the time of writing).
|
||||||
"""
|
"""
|
||||||
async with self._registry_lock:
|
async with self._registry_lock:
|
||||||
if self._count_open_locked() >= self.max_live_sessions:
|
if self._count_open_locked() >= self.max_live_sessions:
|
||||||
|
|
@ -157,11 +182,13 @@ class AcpxManager:
|
||||||
"sessions", "new",
|
"sessions", "new",
|
||||||
"--name", session_id,
|
"--name", session_id,
|
||||||
]
|
]
|
||||||
|
env = _env_with_model(model)
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
shutil.rmtree(cwd, ignore_errors=True)
|
shutil.rmtree(cwd, ignore_errors=True)
|
||||||
|
|
@ -201,6 +228,7 @@ class AcpxManager:
|
||||||
agent=agent,
|
agent=agent,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
created_at=int(time.time()),
|
created_at=int(time.time()),
|
||||||
|
model=model,
|
||||||
)
|
)
|
||||||
self._sessions[session_id] = sess
|
self._sessions[session_id] = sess
|
||||||
return sess
|
return sess
|
||||||
|
|
@ -256,6 +284,7 @@ class AcpxManager:
|
||||||
*cmd,
|
*cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
env=_env_with_model(sess.model),
|
||||||
)
|
)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise AcpxNotInstalled(f"acpx binary not found: {self.acpx_bin}") from e
|
raise AcpxNotInstalled(f"acpx binary not found: {self.acpx_bin}") from e
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,10 @@ class TokenCreateRequest(BaseModel):
|
||||||
|
|
||||||
class CreateSessionRequest(BaseModel):
|
class CreateSessionRequest(BaseModel):
|
||||||
agent: str = Field(default="claude", min_length=1, max_length=64, pattern=r"^[a-zA-Z0-9_-]+$")
|
agent: str = Field(default="claude", min_length=1, max_length=64, pattern=r"^[a-zA-Z0-9_-]+$")
|
||||||
|
# Optional per-session model override; propagated to the underlying claude CLI
|
||||||
|
# via ANTHROPIC_MODEL env. NULL = let the CLI default decide. Examples: "opus",
|
||||||
|
# "sonnet", or a full model id like "claude-opus-4-7".
|
||||||
|
model: str | None = Field(default=None, max_length=64, pattern=r"^[a-zA-Z0-9._-]+$")
|
||||||
meta: dict | None = None
|
meta: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -257,7 +261,9 @@ async def create_session(
|
||||||
rec = auth.require_app(request, authorization)
|
rec = auth.require_app(request, authorization)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sess = await acpx_manager.create(app_name=rec["name"], agent=body.agent)
|
sess = await acpx_manager.create(
|
||||||
|
app_name=rec["name"], agent=body.agent, model=body.model
|
||||||
|
)
|
||||||
except AcpxPoolFull as e:
|
except AcpxPoolFull as e:
|
||||||
raise HTTPException(503, f"session pool full: {e}")
|
raise HTTPException(503, f"session pool full: {e}")
|
||||||
except AcpxNotInstalled as e:
|
except AcpxNotInstalled as e:
|
||||||
|
|
@ -269,6 +275,7 @@ async def create_session(
|
||||||
session_id=sess.session_id,
|
session_id=sess.session_id,
|
||||||
app_name=rec["name"],
|
app_name=rec["name"],
|
||||||
agent=body.agent,
|
agent=body.agent,
|
||||||
|
model=body.model,
|
||||||
cwd=str(sess.cwd),
|
cwd=str(sess.cwd),
|
||||||
meta=body.meta,
|
meta=body.meta,
|
||||||
)
|
)
|
||||||
|
|
@ -276,12 +283,13 @@ async def create_session(
|
||||||
session_id=sess.session_id,
|
session_id=sess.session_id,
|
||||||
app_name=rec["name"],
|
app_name=rec["name"],
|
||||||
event="create",
|
event="create",
|
||||||
meta={"agent": body.agent},
|
meta={"agent": body.agent, "model": body.model},
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"session_id": sess.session_id,
|
"session_id": sess.session_id,
|
||||||
"agent": body.agent,
|
"agent": body.agent,
|
||||||
|
"model": body.model,
|
||||||
"created_at": row["created_at"],
|
"created_at": row["created_at"],
|
||||||
"cwd": str(sess.cwd),
|
"cwd": str(sess.cwd),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,24 @@ class Store:
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
with self._conn() as c:
|
with self._conn() as c:
|
||||||
c.executescript(SCHEMA)
|
c.executescript(SCHEMA)
|
||||||
|
self._migrate_v0_2_1(c)
|
||||||
|
|
||||||
|
def _migrate_v0_2_1(self, c):
|
||||||
|
"""Idempotent additive migrations layered on top of SCHEMA.
|
||||||
|
|
||||||
|
Each ALTER TABLE is wrapped in try/except for OperationalError so
|
||||||
|
re-running on an already-migrated DB is a no-op. New deployments
|
||||||
|
get the columns via this same path immediately after CREATE TABLE.
|
||||||
|
"""
|
||||||
|
# Optional per-session model override (e.g. 'opus' / 'sonnet').
|
||||||
|
# NULL = let underlying claude CLI default decide. Cobb's intent
|
||||||
|
# 2026-04-29: crafting-table's patcher pins 'opus' per session so
|
||||||
|
# code-work prompts get Opus's long-context reasoning while other
|
||||||
|
# consumers (cauldron) keep using whatever the CLI default is.
|
||||||
|
try:
|
||||||
|
c.execute("ALTER TABLE sessions ADD COLUMN model TEXT")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # column already present — re-run on already-migrated DB
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _conn(self):
|
def _conn(self):
|
||||||
|
|
@ -211,14 +229,15 @@ class Store:
|
||||||
app_name: str,
|
app_name: str,
|
||||||
agent: str,
|
agent: str,
|
||||||
cwd: str,
|
cwd: str,
|
||||||
|
model: str | None = None,
|
||||||
meta: dict | None = None,
|
meta: dict | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
meta_json = json.dumps(meta) if meta is not None else None
|
meta_json = json.dumps(meta) if meta is not None else None
|
||||||
with self._conn() as c:
|
with self._conn() as c:
|
||||||
c.execute(
|
c.execute(
|
||||||
"INSERT INTO sessions (session_id, app_name, agent, cwd, created_at, meta_json) VALUES (?,?,?,?,?,?)",
|
"INSERT INTO sessions (session_id, app_name, agent, model, cwd, created_at, meta_json) VALUES (?,?,?,?,?,?,?)",
|
||||||
(session_id, app_name, agent, cwd, now, meta_json),
|
(session_id, app_name, agent, model, cwd, now, meta_json),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"session_id": session_id,
|
"session_id": session_id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue