refactor: use a single agent model path

This commit is contained in:
wuyang
2026-07-26 18:50:51 +08:00
parent 2e8e81c790
commit d95d06233f
23 changed files with 330 additions and 454 deletions
+27 -30
View File
@@ -1,15 +1,8 @@
# K1412 Agent
K1412 Agent is a multi-user web Agent built around two deliberately different
experiences:
- **Chat** is fast, conversational, and uses Open WebUI's native tool loop.
- **Work** is a long-running coding Agent whose loop, scheduler, context,
memory, tools, and sub-agents are owned by this repository.
A conversation may be upgraded from Chat to Work. The upgrade is intentionally
one-way so that two independent Agent loops never compete for the same
conversation state.
K1412 Agent is a multi-user web coding Agent. Every user request enters the
same independently evolvable Agent loop; its scheduler, context policy, memory,
tools, evidence rules, and child Agents are owned by this repository.
The production instance is available at <https://agent.k1412.top>.
@@ -22,7 +15,7 @@ browser
Open WebUI (auth, RBAC, chat history, UI)
|
v
Agent Runtime (model gateway + Work loop)
Agent Runtime (model gateway + Agent loop)
|
v
Workspace Gateway (identity, policy, audit)
@@ -31,19 +24,24 @@ Workspace Gateway (identity, policy, audit)
one Docker workspace per Open WebUI user on a dedicated execution host
```
Open WebUI is pinned and lightly patched. Its model picker is replaced by two
product-level choices: `Chat / Work` and `轻度 / 中 / 高 / 极高`. Provider URLs,
provider model IDs, API keys, tool server settings, system prompts, and runtime
parameters remain server-side.
Open WebUI is pinned and lightly patched. Its picker exposes only four model
names and a separate read-only thinking status. Provider URLs, provider model
IDs, API keys, tool server settings, system prompts, and runtime parameters
remain server-side.
The inference mapping is fixed:
Model identity and thinking control are intentionally separate:
| Strength | Provider model |
| --- | --- |
| 轻度 | `ChatGPT-5.6:Luna` |
| | `ChatGPT-5.6:Terra` |
| | `ChatGPT-5.6:Sol` |
| 极高 | `deepseek-v4-pro`Thinking`reasoning_effort=max` |
| UI model | Provider model | Thinking | Reasoning effort |
| --- | --- | --- | --- |
| Luna | `ChatGPT-5.6:Luna` | On, no effort levels | — |
| Terra | `ChatGPT-5.6:Terra` | On, no effort levels | — |
| Sol | `ChatGPT-5.6:Sol` | On, no effort levels | — |
| DeepSeek V4 Pro | `deepseek-v4-pro` | On | Extreme (`max`) |
The three Ollama models advertise `thinking`, but they are Qwen-family models
with a boolean thinking switch rather than GPT-OSS-style low/medium/high
effort levels. Luna, Terra, and Sol are different models, not three reasoning
settings for one model.
## Local development
@@ -64,9 +62,8 @@ The inference mapping is fixed:
4. Open <http://localhost:3000>. New users register as `pending` and require
approval by the bootstrap administrator.
The pasted provider credential from the planning conversation is intentionally
not stored here. Rotate it and place the replacement only in the protected
deployment `.env`.
Provider credentials are intentionally not stored here. Keep them only in the
protected deployment `.env`.
## Tests
@@ -83,14 +80,14 @@ test, with:
./scripts/verify.sh
```
Run a live Work evaluation in a unique, disposable, network-disabled Docker
Run a live Agent evaluation in a unique, disposable, network-disabled Docker
workspace with:
```bash
set -a
source /path/to/protected/deepseek.env
set +a
.venv/bin/python scripts/eval-live-work.py --model work-extreme
.venv/bin/python scripts/eval-live-work.py --model deepseek-v4-pro
```
The evaluator reports latency, token usage, tool counts, completion evidence,
@@ -98,8 +95,8 @@ and the generated file listing. It removes only the uniquely named evaluation
container, volume, and network unless `--keep-workspace` is supplied.
Run the disposable six-service integration stack with a deterministic fake
model provider and exercise registration approval, both Agent loops, workspace
isolation, and the one-way mode upgrade with:
model provider and exercise registration approval, the Agent loop, workspace
isolation, and file delivery with:
```bash
./scripts/verify-e2e.sh
@@ -116,7 +113,7 @@ High/Critical image vulnerabilities, with:
./scripts/audit-images.sh
```
Generated files are available from the **工作区文件** button beside the mode
Generated files are available from the **工作区文件** button beside the model
selector. Users can browse directories, download one file, or download the
current directory as a `.tar.gz` archive. Browser requests are authenticated by
Open WebUI and re-signed for the Workspace Gateway; no internal key is exposed.
+26 -9
View File
@@ -9,6 +9,10 @@ import httpx
from agent_platform.models import MODEL_SPECS
LEGACY_MODEL_IDS = {
f"{mode}-{strength}" for mode in ("chat", "work") for strength in ("light", "medium", "high", "extreme")
}
def _required(name: str) -> str:
value = os.getenv(name, "").strip()
@@ -33,27 +37,24 @@ def _model_payload() -> list[dict[str, Any]]:
public_read = [{"principal_type": "user", "principal_id": "*", "permission": "read"}]
items = []
for spec in MODEL_SPECS.values():
is_chat = spec.mode == "chat"
items.append(
{
"id": spec.public_id,
"base_model_id": None,
"name": spec.display_name,
"params": {"function_calling": "native"} if is_chat else {},
"params": {},
"meta": {
"description": (
"快速问答,使用原生工具循环。" if is_chat else "长链路工作,使用 K1412 Work Agent。"
),
"toolIds": ["server:workspace"] if is_chat else [],
"description": (f"K1412 Agent;思考:{spec.thinking_label}"),
"toolIds": [],
"capabilities": {
"tool_calling": is_chat,
"tool_calling": False,
"builtin_tools": False,
"file_context": False,
"citations": False,
"vision": False,
"file_upload": False,
},
"tags": [{"name": "Chat" if is_chat else "Work"}],
"tags": [{"name": "Agent"}],
},
"access_grants": public_read,
"is_active": True,
@@ -151,6 +152,22 @@ async def bootstrap() -> None:
)
tool_server_response.raise_for_status()
for model_id in sorted(LEGACY_MODEL_IDS):
existing = await client.get(
f"{base_url}/api/v1/models/model",
headers=headers,
params={"id": model_id},
)
if existing.status_code == 404:
continue
existing.raise_for_status()
delete_response = await client.post(
f"{base_url}/api/v1/models/model/delete",
headers=headers,
json={"id": model_id},
)
delete_response.raise_for_status()
model_response = await client.post(
f"{base_url}/api/v1/models/import",
headers=headers,
@@ -158,7 +175,7 @@ async def bootstrap() -> None:
)
model_response.raise_for_status()
print("Open WebUI bootstrap complete: auth policy, workspace tools, and eight fixed Agent entries are ready.")
print("Open WebUI bootstrap complete: auth policy, workspace tools, and four Agent models are ready.")
def run() -> None:
+59 -35
View File
@@ -3,8 +3,6 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
Mode = Literal["chat", "work"]
Strength = Literal["light", "medium", "high", "extreme"]
Provider = Literal["k1412", "deepseek"]
@@ -12,49 +10,70 @@ Provider = Literal["k1412", "deepseek"]
class ModelSpec:
public_id: str
display_name: str
mode: Mode
strength: Strength
provider: Provider
provider_model: str
thinking_enabled: bool
thinking_label: str
thinking_adjustable: bool
reasoning_effort: str | None
max_output_tokens: int
max_iterations: int
context_char_budget: int
_TIERS = {
"light": ("轻度", "k1412", "ChatGPT-5.6:Luna", False, None, 4_096, 8, 120_000),
"medium": ("", "k1412", "ChatGPT-5.6:Terra", False, None, 4_096, 16, 240_000),
"high": ("", "k1412", "ChatGPT-5.6:Sol", False, None, 4_096, 24, 400_000),
"extreme": ("极高", "deepseek", "deepseek-v4-pro", True, "max", 16_384, 32, 800_000),
}
MODEL_SPECS: dict[str, ModelSpec] = {
f"{mode}-{strength}": ModelSpec(
public_id=f"{mode}-{strength}",
display_name=f"{'Chat' if mode == 'chat' else 'Work'} · {label}",
mode=mode,
strength=strength,
provider=provider,
provider_model=provider_model,
thinking_enabled=thinking_enabled,
reasoning_effort=reasoning_effort,
max_output_tokens=max_output_tokens,
max_iterations=max_iterations,
context_char_budget=context_budget,
)
for mode in ("chat", "work")
for strength, (
label,
provider,
provider_model,
thinking_enabled,
reasoning_effort,
max_output_tokens,
max_iterations,
context_budget,
) in _TIERS.items()
"luna": ModelSpec(
public_id="luna",
display_name="Luna",
provider="k1412",
provider_model="ChatGPT-5.6:Luna",
thinking_enabled=True,
thinking_label="开启(不分档)",
thinking_adjustable=False,
reasoning_effort=None,
max_output_tokens=4_096,
max_iterations=8,
context_char_budget=120_000,
),
"terra": ModelSpec(
public_id="terra",
display_name="Terra",
provider="k1412",
provider_model="ChatGPT-5.6:Terra",
thinking_enabled=True,
thinking_label="开启(不分档)",
thinking_adjustable=False,
reasoning_effort=None,
max_output_tokens=4_096,
max_iterations=16,
context_char_budget=240_000,
),
"sol": ModelSpec(
public_id="sol",
display_name="Sol",
provider="k1412",
provider_model="ChatGPT-5.6:Sol",
thinking_enabled=True,
thinking_label="开启(不分档)",
thinking_adjustable=False,
reasoning_effort=None,
max_output_tokens=4_096,
max_iterations=24,
context_char_budget=400_000,
),
"deepseek-v4-pro": ModelSpec(
public_id="deepseek-v4-pro",
display_name="DeepSeek V4 Pro",
provider="deepseek",
provider_model="deepseek-v4-pro",
thinking_enabled=True,
thinking_label="极高",
thinking_adjustable=False,
reasoning_effort="max",
max_output_tokens=16_384,
max_iterations=32,
context_char_budget=800_000,
),
}
@@ -74,6 +93,11 @@ def openai_model_list() -> dict:
"object": "model",
"owned_by": "k1412-agent",
"name": spec.display_name,
"k1412": {
"thinking_enabled": spec.thinking_enabled,
"thinking_label": spec.thinking_label,
"thinking_adjustable": spec.thinking_adjustable,
},
}
for spec in MODEL_SPECS.values()
],
+1 -1
View File
@@ -1 +1 @@
"""Model gateway and independently evolvable Work Agent runtime."""
"""Model gateway and independently evolvable K1412 Agent runtime."""
+4 -14
View File
@@ -10,7 +10,7 @@ from typing import Annotated, Any
import httpx
import uvicorn
from fastapi import FastAPI, Header, HTTPException, status
from fastapi import FastAPI, Header, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
from agent_platform.auth import UserIdentity, decode_openwebui_identity, verify_service_bearer
@@ -198,16 +198,6 @@ def create_app(
return await forward_direct()
stable_chat_id = (chat_id or message_id or f"ephemeral-{uuid.uuid4().hex}").strip()
selected_mode = await app.state.store.select_mode(identity.user_id, chat_id or "", spec.mode)
if selected_mode == "work" and spec.mode == "chat":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="This conversation has been upgraded to Work and cannot return to Chat.",
)
if spec.mode == "chat":
return await forward_direct()
messages = [message.model_dump(exclude_none=True) for message in body.messages]
if not body.stream:
@@ -224,7 +214,7 @@ def create_app(
)
return _completion(body.model, answer)
async def work_stream() -> AsyncIterator[bytes]:
async def agent_stream() -> AsyncIterator[bytes]:
queue: asyncio.Queue[tuple[str, Any]] = asyncio.Queue()
async def publish(event_type: str, payload: dict[str, Any]) -> None:
@@ -255,7 +245,7 @@ def create_app(
if kind == "done":
break
if kind == "error":
yield _sse_chunk(body.model, f"\n\nWork 运行失败:{value}")
yield _sse_chunk(body.model, f"\n\nAgent 运行失败:{value}")
continue
if kind == "answer":
text = str(value)
@@ -271,7 +261,7 @@ def create_app(
await asyncio.gather(task, return_exceptions=True)
return StreamingResponse(
work_stream(),
agent_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
+3 -3
View File
@@ -28,7 +28,7 @@ from agent_platform.store import RuntimeStore
EventCallback = Callable[[str, dict[str, Any]], Awaitable[None]]
WORK_SYSTEM_PROMPT = """\
You are Work, an autonomous coding Agent operating in one isolated user workspace at /workspace.
You are K1412 Agent, an autonomous coding Agent operating in one isolated user workspace at /workspace.
Own the outcome: inspect the workspace, form a plan for non-trivial work, make scoped changes, run
relevant verification, and report concrete results. Use tools instead of inventing file contents or
@@ -514,8 +514,8 @@ class AgentLoop:
await recorder.emit(
"run.created",
{
"model_tier": spec.strength,
"strategy_version": "work-loop-v2",
"model_id": spec.public_id,
"strategy_version": "agent-loop-v3",
"scheduler_version": "safe-parallel-v1",
"context_policy": "recent-visible-v1",
},
+3 -3
View File
@@ -200,14 +200,14 @@ TOOL_METADATA: dict[str, ToolMetadata] = {
),
"remember": ToolMetadata(
"remember",
"Store a durable user preference or fact that will help future Work tasks.",
"Store a durable user preference or fact that will help future Agent tasks.",
object_schema({"content": {"type": "string"}}, ["content"]),
False,
False,
),
"recall_memory": ToolMetadata(
"recall_memory",
"Search durable Work memory for this user.",
"Search durable Agent memory for this user.",
object_schema(
{
"query": {"type": "string", "default": ""},
@@ -219,7 +219,7 @@ TOOL_METADATA: dict[str, ToolMetadata] = {
),
"forget_memory": ToolMetadata(
"forget_memory",
"Delete one durable Work memory by id.",
"Delete one durable Agent memory by id.",
object_schema({"memory_id": {"type": "string"}}, ["memory_id"]),
False,
False,
-45
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import asyncio
import uuid
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
@@ -16,17 +15,6 @@ class Base(DeclarativeBase):
pass
class ConversationMode(Base):
__tablename__ = "agent_conversation_modes"
__table_args__ = (UniqueConstraint("user_id", "chat_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[str] = mapped_column(String(128), index=True)
chat_id: Mapped[str] = mapped_column(String(256), index=True)
mode: Mapped[str] = mapped_column(String(16))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class RunEvent(Base):
__tablename__ = "agent_run_events"
@@ -64,8 +52,6 @@ class RuntimeStore:
def __init__(self, url: str) -> None:
self.engine: AsyncEngine = create_async_engine(url, pool_pre_ping=True)
self.sessions = async_sessionmaker(self.engine, expire_on_commit=False)
self._mode_locks: dict[tuple[str, str], asyncio.Lock] = {}
self._lock_guard = asyncio.Lock()
async def initialize(self) -> None:
async with self.engine.begin() as connection:
@@ -79,37 +65,6 @@ class RuntimeStore:
async with self.sessions() as session:
yield session
async def _mode_lock(self, user_id: str, chat_id: str) -> asyncio.Lock:
key = (user_id, chat_id)
async with self._lock_guard:
return self._mode_locks.setdefault(key, asyncio.Lock())
async def select_mode(self, user_id: str, chat_id: str, requested: str) -> str:
if requested not in {"chat", "work"}:
raise ValueError("Invalid conversation mode")
if not chat_id:
return requested
lock = await self._mode_lock(user_id, chat_id)
async with lock:
async with self.session() as session:
row = await session.scalar(
select(ConversationMode).where(
ConversationMode.user_id == user_id,
ConversationMode.chat_id == chat_id,
)
)
if row is None:
session.add(ConversationMode(user_id=user_id, chat_id=chat_id, mode=requested))
await session.commit()
return requested
if row.mode == "work" and requested == "chat":
return "work"
if row.mode != requested:
row.mode = requested
row.updated_at = datetime.now(UTC)
await session.commit()
return row.mode
async def append_event(
self,
run_id: str,
+2 -2
View File
@@ -35,8 +35,8 @@ services:
K1412_GATEWAY_KEY: ${INTERNAL_GATEWAY_KEY:?INTERNAL_GATEWAY_KEY is required}
OPENAI_API_BASE_URL: http://runtime:8000/v1
OPENAI_API_KEY: ${INTERNAL_PROVIDER_KEY:?INTERNAL_PROVIDER_KEY is required}
DEFAULT_MODELS: chat-medium
TASK_MODEL: chat-light
DEFAULT_MODELS: terra
TASK_MODEL: luna
ENABLE_TITLE_GENERATION: "false"
ENABLE_FOLLOW_UP_GENERATION: "false"
ENABLE_TAGS_GENERATION: "false"
+2 -2
View File
@@ -35,8 +35,8 @@ services:
K1412_GATEWAY_KEY: ${INTERNAL_GATEWAY_KEY:?INTERNAL_GATEWAY_KEY is required}
OPENAI_API_BASE_URL: http://runtime:8000/v1
OPENAI_API_KEY: ${INTERNAL_PROVIDER_KEY:?INTERNAL_PROVIDER_KEY is required}
DEFAULT_MODELS: chat-medium
TASK_MODEL: chat-light
DEFAULT_MODELS: terra
TASK_MODEL: luna
ENABLE_TITLE_GENERATION: "false"
ENABLE_FOLLOW_UP_GENERATION: "false"
ENABLE_TAGS_GENERATION: "false"
+29 -67
View File
@@ -7,96 +7,58 @@
export let showSetDefault = true;
let showWorkspace = false;
const strengths = [
{ key: 'light', label: '轻度' },
{ key: 'medium', label: '' },
{ key: 'high', label: '' },
{ key: 'extreme', label: '极高' }
const modelOptions = [
{ id: 'luna', label: 'Luna', thinking: '开启 · 不分档' },
{ id: 'terra', label: 'Terra', thinking: '开启 · 不分档' },
{ id: 'sol', label: 'Sol', thinking: '开启 · 不分档' },
{ id: 'deepseek-v4-pro', label: 'DeepSeek', thinking: '极高' }
];
const allowed = new Set(modelOptions.map((item) => item.id));
const allowed = new Set([
'chat-light',
'chat-medium',
'chat-high',
'chat-extreme',
'work-light',
'work-medium',
'work-high',
'work-extreme'
]);
$: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'chat-medium';
$: mode = selected.startsWith('work-') ? 'work' : 'chat';
$: strength = selected.split('-')[1] ?? 'medium';
$: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'terra';
$: selectedOption = modelOptions.find((item) => item.id === selected) ?? modelOptions[1];
$: availableIds = new Set($models.map((model) => model.id));
$: if ($models.length > 0 && !availableIds.has(selectedModels?.[0])) {
const fallback = availableIds.has('chat-medium')
? 'chat-medium'
: [...allowed].find((id) => availableIds.has(id));
const fallback = availableIds.has('terra')
? 'terra'
: modelOptions.find((item) => availableIds.has(item.id))?.id;
if (fallback) selectedModels = [fallback];
}
const chooseMode = (nextMode: 'chat' | 'work') => {
if (disabled || (mode === 'work' && nextMode === 'chat')) return;
const next = `${nextMode}-${strength}`;
if (availableIds.has(next)) selectedModels = [next];
};
const chooseStrength = (nextStrength: string) => {
if (disabled) return;
const next = `${mode}-${nextStrength}`;
if (availableIds.has(next)) selectedModels = [next];
const chooseModel = (modelId: string) => {
if (disabled || !availableIds.has(modelId)) return;
selectedModels = [modelId];
};
</script>
<div class="flex max-w-fit items-center gap-2 select-none">
<div class="flex max-w-full items-center gap-2 select-none">
<div
class="flex items-center rounded-xl bg-gray-100/90 p-1 text-sm dark:bg-gray-800/90"
aria-label="Agent mode"
class="flex items-center overflow-x-auto rounded-xl border border-gray-200/80 p-1 text-xs dark:border-gray-700/80"
aria-label="模型"
>
{#each modelOptions as item}
<button
type="button"
class="rounded-lg px-3 py-1.5 font-medium transition {mode === 'chat'
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
: 'text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => chooseMode('chat')}
disabled={disabled || mode === 'work'}
title={mode === 'work' ? '此对话已升级为 Work,不能返回 Chat' : '快速对话'}
>
Chat
</button>
<button
type="button"
class="rounded-lg px-3 py-1.5 font-medium transition {mode === 'work'
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
: 'text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'}"
on:click={() => chooseMode('work')}
{disabled}
title="长链路、多步骤工作"
>
Work
</button>
</div>
<div
class="flex items-center rounded-xl border border-gray-200/80 p-1 text-xs dark:border-gray-700/80"
aria-label="Inference strength"
>
{#each strengths as item}
<button
type="button"
class="rounded-lg px-2.5 py-1.5 transition {strength === item.key
class="whitespace-nowrap rounded-lg px-2.5 py-1.5 font-medium transition {selected === item.id
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
: 'text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800'}"
on:click={() => chooseStrength(item.key)}
{disabled}
on:click={() => chooseModel(item.id)}
disabled={disabled || ($models.length > 0 && !availableIds.has(item.id))}
title={item.id === 'deepseek-v4-pro' ? 'DeepSeek V4 Pro' : item.label}
>
{item.label}
</button>
{/each}
</div>
<div
class="whitespace-nowrap rounded-xl bg-gray-100/90 px-2.5 py-2 text-xs text-gray-500 dark:bg-gray-800/90 dark:text-gray-400"
title="模型与思考能力是两个独立概念"
>
思考 <span class="font-medium text-gray-800 dark:text-gray-200">{selectedOption.thinking}</span>
</div>
<button
type="button"
class="rounded-xl border border-gray-200/80 p-2 text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:border-gray-700/80 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
+15 -27
View File
@@ -1,25 +1,12 @@
# Architecture
## Product modes
## Single Agent path
Chat and Work share the same authenticated user, conversation history, and
workspace, but not the same Agent loop.
Open WebUI owns authenticated users, conversation history, and rendering.
Every normal model request is delegated to the K1412 Runtime. There is no
second Chat loop and no per-conversation mode state.
| Concern | Chat | Work |
| --- | --- | --- |
| Primary purpose | Fast conversation | Multi-step deliverables |
| Loop owner | Open WebUI native loop | K1412 Agent Runtime |
| Tools | Workspace Gateway through OpenAPI | Workspace Gateway through Runtime |
| Visible history | Open WebUI | Open WebUI |
| Run events | Open WebUI | Runtime event store |
| Context and memory | Open WebUI defaults are disabled by policy | Runtime policy and store |
| Workspace files | Authenticated K1412 file browser | Same file browser and volume |
Selecting Work atomically upgrades `(user_id, chat_id)` in the Runtime store.
Subsequent Chat requests for that conversation are rejected even if a client
tries to bypass the disabled UI control.
## Work loop
## Agent loop
The first strategy is intentionally modular:
@@ -27,21 +14,22 @@ The first strategy is intentionally modular:
- `ContextPolicy` owns visible-message cleanup and compaction.
- `ToolRegistry` owns tool schemas and dispatch.
- `AgentLoop` owns iteration, safe parallel scheduling, and delegation.
- `RuntimeStore` owns modes, events, plans, and durable Work memory.
- `RuntimeStore` owns events, plans, and durable Agent memory.
- `ExecutionProvider` owns local or SSH Docker execution.
Every run records its model tier, strategy version, scheduler version, context
Every run records its model ID, strategy version, scheduler version, context
policy, model/tool events, failures, and completion. This event stream is the
foundation for replay, evaluation, and future experiment assignment.
The light, medium, and high tiers use the internal K1412 model endpoint. The
extreme tier uses DeepSeek V4 Pro with thinking enabled and maximum reasoning
effort. Provider selection and credentials remain server-side. Thinking state
is preserved across tool turns because DeepSeek requires the assistant's
`reasoning_content` to be returned with the following tool results; Work never
publishes that hidden state as its own user-facing reasoning.
Luna, Terra, and Sol are three independent Ollama models. Each advertises
boolean thinking support, but none advertises adjustable reasoning effort.
DeepSeek V4 Pro is a fourth model configured with thinking enabled and maximum
reasoning effort. Provider selection and credentials remain server-side.
Thinking state is preserved across tool turns when the provider requires the
assistant's `reasoning_content` to be returned with following tool results.
The Agent never publishes hidden reasoning as its own user-facing output.
Work completion is evidence-gated. Requests for scripts, reports, code, or
Completion is evidence-gated. Requests for scripts, reports, code, or
other concrete artifacts are not accepted as complete until a file mutation
has succeeded and a later read, diff, or command has verified the latest
mutation. Bare interactive shell commands are rejected. If the iteration
+4 -3
View File
@@ -27,6 +27,7 @@ them does not change the Agent loop:
| k1412 homepage | Service discovery entry |
| Tailscale | Private NAS-to-execution-host network |
| Unraid Compose Manager | Application lifecycle on the NAS |
| Ollama + 32 GB NVIDIA GPU | Luna, Terra, and Sol model serving |
Other NAS applications are independent tenants. They are not dependencies of
K1412 Agent merely because they share the host.
@@ -35,8 +36,8 @@ K1412 Agent merely because they share the host.
| Container | Responsibility | Agent core? |
| --- | --- | --- |
| `k1412-agent-web` | Open WebUI auth, RBAC, history, UI, Chat loop | Product shell |
| `k1412-agent-runtime` | Work loop and provider gateway | Yes |
| `k1412-agent-web` | Open WebUI auth, RBAC, history, and UI | Product shell |
| `k1412-agent-runtime` | Agent loop and provider gateway | Yes |
| `k1412-agent-gateway` | Identity, execution policy, workspace lifecycle | Yes |
| `k1412-agent-postgres` | Open WebUI and Runtime durable state | State |
| `k1412-agent-redis` | Open WebUI coordination/cache | State |
@@ -68,7 +69,7 @@ The code intended for continuous experimentation is:
- `agent_platform/runtime/loop.py`: loop and evidence policy;
- `agent_platform/runtime/context.py`: context policy;
- `agent_platform/runtime/tools.py`: tools and scheduling metadata;
- `agent_platform/store.py`: events, plans, mode state, memory;
- `agent_platform/store.py`: events, plans, and memory;
- `agent_platform/gateway/`: isolated execution providers.
Frontend branding, reverse proxy configuration, and registry automation are
+9 -10
View File
@@ -3,14 +3,13 @@
## Responsibility split
Open WebUI is the product shell. It owns account registration, login sessions,
RBAC, administrator approval, conversation history, rendering, and the Chat
mode Agent loop. K1412 does not fork those responsibilities into a second
authentication or chat database.
RBAC, administrator approval, conversation history, and rendering. K1412 does
not fork those responsibilities into a second authentication or chat database.
K1412 Runtime owns Work mode: context selection, durable Work memory, planning,
tool schemas, scheduling, parallel reads, child Agents, evidence gating,
experiments, and run-event persistence. Workspace Gateway owns identity-bound
execution policy and Docker lifecycle.
K1412 Runtime owns the only user-facing Agent loop: context selection, durable
memory, planning, tool schemas, scheduling, parallel reads, child Agents,
evidence gating, experiments, and run-event persistence. Workspace Gateway
owns identity-bound execution policy and Docker lifecycle.
This split makes the expensive experimental surface small. A new scheduler or
context policy changes Runtime, not the account system or entire frontend.
@@ -19,7 +18,7 @@ context policy changes Runtime, not the account system or entire frontend.
Open WebUI natively understands tool-call and reasoning detail blocks. It also
has a richer live status mechanism for loops that execute inside its own
backend. Work is an external OpenAI-compatible provider, so it cannot mutate
backend. K1412 Runtime is an external OpenAI-compatible provider, so it cannot mutate
Open WebUI's internal socket status objects directly.
K1412 therefore emits Open WebUI-compatible completed tool detail blocks. Each
@@ -52,8 +51,8 @@ Upgrading Open WebUI requires:
1. updating the tag and commit pin;
2. replaying every patch with `git apply`;
3. building the Svelte production bundle;
4. running authentication, Chat, Work, and file-browser E2E tests;
4. running authentication, Agent-loop, isolation, and file-browser E2E tests;
5. reviewing upstream license changes.
The K1412 Agent loop is not embedded into Open WebUI, so most Work experiments
The K1412 Agent loop is not embedded into Open WebUI, so most Agent experiments
do not incur this upgrade cost.
+27 -4
View File
@@ -41,12 +41,35 @@ proxy.
Set `DEEPSEEK_API_BASE_URL=https://api.deepseek.com` and place
`DEEPSEEK_API_KEY` only in the protected deployment `.env`. The key is used
only by Runtime for the extreme tier and must never be added to a browser,
only by Runtime for DeepSeek V4 Pro and must never be added to a browser,
Compose file, image, repository, or log.
The SSH override removes `/var/run/docker.sock` from Gateway and mounts the
dedicated SSH directory read-only at `/root/.ssh`.
## Ollama model residency
The NAS Ollama service keeps Luna, Terra, and Sol resident with:
- `OLLAMA_KEEP_ALIVE=-1`;
- `OLLAMA_MAX_LOADED_MODELS=3`;
- `OLLAMA_NUM_PARALLEL=1`;
- `OLLAMA_GPU_OVERHEAD=2147483648`;
- Flash Attention and `q8_0` KV cache enabled.
The three 32K-context runners were measured at approximately 2.88 GB, 4.12 GB,
and 18.44 GB of GPU allocation respectively. Their combined allocation is
about 25.44 GB on the 32 GB GPU. Do not increase context size or per-model
parallelism without repeating the simultaneous-load test and leaving capacity
for inference overhead.
The persistent Unraid template is
`/boot/config/plugins/dockerMan/templates-user/my-ollama.xml`. A locked,
idempotent five-minute check in
`/boot/config/plugins/dynamix/k1412-ollama.cron` prewarms only missing models
after a host or container restart. Confirm `/api/ps` lists all three with an
infinite expiry after recovery.
## Workspace migration
Before changing execution hosts:
@@ -78,9 +101,9 @@ RUN_DOCKER_INTEGRATION=1 .venv/bin/pytest tests/test_docker_workspace.py
Production smoke checks must cover:
- anonymous requests are redirected/rejected;
- an approved user can use Chat;
- Chat can upgrade to Work but cannot downgrade;
- Work creates and verifies a real file;
- an approved user sees exactly four Agent models;
- each model routes through the custom Agent loop;
- the Agent creates and verifies a real file;
- the file browser lists and downloads that file;
- a second user cannot see it;
- Gateway health reports `ssh-docker`;
+3 -4
View File
@@ -69,12 +69,11 @@ class StubHandler(BaseHTTPRequestHandler):
available_names = {
str((tool.get("function") or {}).get("name", "")) for tool in tools if isinstance(tool, dict)
}
is_work = "update_plan" in available_names
is_agent = "update_plan" in available_names
if tools and not has_tool_result:
path = "work-proof.txt" if is_work else "chat-proof.txt"
arguments = json.dumps(
{"path": path, "content": _last_user_text(messages) or "e2e"},
{"path": "work-proof.txt", "content": _last_user_text(messages) or "e2e"},
ensure_ascii=False,
)
message = {
@@ -91,7 +90,7 @@ class StubHandler(BaseHTTPRequestHandler):
self._respond(payload, _completion(model, message, "tool_calls"))
return
if is_work and "write_file" in called_tools and "read_file" not in called_tools:
if is_agent and "write_file" in called_tools and "read_file" not in called_tools:
message = {
"role": "assistant",
"content": None,
+24 -36
View File
@@ -26,14 +26,10 @@ INTERNAL_PROVIDER_KEY = os.getenv(
"e2e-provider-secret-at-least-32-bytes",
)
EXPECTED_MODELS = {
"chat-light",
"chat-medium",
"chat-high",
"chat-extreme",
"work-light",
"work-medium",
"work-high",
"work-extreme",
"luna",
"terra",
"sol",
"deepseek-v4-pro",
}
@@ -56,7 +52,6 @@ def _stream_chat(
model: str,
chat_id: str,
prompt: str,
include_workspace_tools: bool,
) -> tuple[int, str]:
user_message_id = uuid.uuid4().hex
assistant_message_id = uuid.uuid4().hex
@@ -75,11 +70,9 @@ def _stream_chat(
"content": prompt,
"timestamp": int(time.time()),
},
"params": {"function_calling": "native"},
"params": {},
"features": {},
}
if include_workspace_tools:
payload["tool_ids"] = ["server:workspace"]
with client.stream(
"POST",
"/api/chat/completions",
@@ -208,37 +201,25 @@ def main() -> None:
chat_ids = [f"local:e2e-{suffix}-a", f"local:e2e-{suffix}-b"]
for index, user in enumerate(users):
marker = f"E2E_CHAT_USER_{index}_{suffix}"
marker = f"E2E_AGENT_USER_{index}_{suffix}"
status_code, body = _stream_chat(
client,
user["token"],
model="chat-light",
model="luna" if index == 0 else "terra",
chat_id=chat_ids[index],
prompt=marker,
include_workspace_tools=True,
)
assert status_code == 200, body
docker_client = docker.from_env()
assert _workspace_file(docker_client, users[0]["id"], "chat-proof.txt") == f"E2E_CHAT_USER_0_{suffix}"
assert _workspace_file(docker_client, users[1]["id"], "chat-proof.txt") == f"E2E_CHAT_USER_1_{suffix}"
assert _workspace_file(docker_client, users[0]["id"], "work-proof.txt") == f"E2E_AGENT_USER_0_{suffix}"
assert _workspace_file(docker_client, users[1]["id"], "work-proof.txt") == f"E2E_AGENT_USER_1_{suffix}"
for user in users:
ref = workspace_ref(user["id"])
workspace = docker_client.containers.get(ref.container_name)
workspace.reload()
assert set(workspace.attrs["NetworkSettings"]["Networks"]) == {ref.network_name}
work_marker = f"E2E_WORK_USER_0_{suffix}"
status_code, work_body = _stream_chat(
client,
users[0]["token"],
model="work-medium",
chat_id=chat_ids[0],
prompt=work_marker,
include_workspace_tools=False,
)
assert status_code == 200, work_body
assert _workspace_file(docker_client, users[0]["id"], "work-proof.txt") == work_marker
listing = client.get(
"/api/v1/k1412/workspace/files",
headers=_auth(users[0]["token"]),
@@ -252,14 +233,21 @@ def main() -> None:
params={"path": "work-proof.txt"},
)
download.raise_for_status()
assert download.text == work_marker
assert download.text == f"E2E_AGENT_USER_0_{suffix}"
isolated_listing = client.get(
"/api/v1/k1412/workspace/files",
headers=_auth(users[1]["token"]),
params={"path": "."},
)
isolated_listing.raise_for_status()
assert "work-proof.txt" not in {item["name"] for item in isolated_listing.json()["entries"]}
assert "work-proof.txt" in {item["name"] for item in isolated_listing.json()["entries"]}
isolated_download = client.get(
"/api/v1/k1412/workspace/download",
headers=_auth(users[1]["token"]),
params={"path": "work-proof.txt"},
)
isolated_download.raise_for_status()
assert isolated_download.text == f"E2E_AGENT_USER_1_{suffix}"
archive = client.get(
"/api/v1/k1412/workspace/archive",
headers=_auth(users[0]["token"]),
@@ -276,20 +264,20 @@ def main() -> None:
event_types = {event["type"] for event in events.json()["items"]}
assert {"run.created", "tool.completed", "run.completed"} <= event_types
downgrade = runtime.post(
legacy = runtime.post(
"/v1/chat/completions",
headers=runtime_headers,
json={
"model": "chat-high",
"messages": [{"role": "user", "content": "THIS_DOWNGRADE_MUST_FAIL"}],
"model": "work-high",
"messages": [{"role": "user", "content": "LEGACY_MODEL_MUST_FAIL"}],
"stream": False,
},
)
assert downgrade.status_code == 409, downgrade.text
assert legacy.status_code == 404, legacy.text
print(
"E2E passed: auth approval, eight models, Chat tools, Work evidence, file downloads, "
"isolation, and downgrade lock."
"E2E passed: auth approval, four Agent models, custom loop evidence, file downloads, "
"and per-user workspace isolation."
)
+2 -4
View File
@@ -119,9 +119,9 @@ class DirectDockerRegistry:
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the real Work loop against a live provider in a disposable Docker workspace."
description="Run the real Agent loop against a live provider in a disposable Docker workspace."
)
parser.add_argument("--model", default="work-extreme")
parser.add_argument("--model", default="deepseek-v4-pro")
parser.add_argument(
"--prompt",
default="写脚本对比一下几个常见排序算法,给一个报告",
@@ -153,8 +153,6 @@ async def cleanup_workspace(execution: DockerExecutionProvider, user_id: str) ->
async def main() -> None:
args = parse_args()
spec = get_model_spec(args.model)
if spec.mode != "work":
raise SystemExit("--model must select a Work tier")
if spec.provider == "deepseek" and not os.getenv("DEEPSEEK_API_KEY", "").strip():
raise SystemExit("DEEPSEEK_API_KEY is required")
+14 -14
View File
@@ -201,7 +201,7 @@ async def test_tool_call_batch_is_bounded(settings) -> None:
try:
answer = await loop.run(
spec=get_model_spec("work-medium"),
spec=get_model_spec("terra"),
messages=[{"role": "user", "content": "检查当前状态"}],
identity=UserIdentity("u1", "u1@example.test", "U1", "user"),
raw_user_jwt="jwt",
@@ -245,7 +245,7 @@ async def test_read_only_tool_calls_run_in_parallel(settings) -> None:
try:
answer = await loop.run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "inspect both"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -314,7 +314,7 @@ async def test_mutating_tool_calls_are_serialized(settings) -> None:
try:
await loop.run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "write both"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -374,7 +374,7 @@ async def test_artifact_completion_is_rejected_until_written_and_verified(settin
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写一个报告"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -469,7 +469,7 @@ async def test_script_and_report_completion_requires_both_files(settings) -> Non
store,
max_tool_output_chars=10_000,
).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写一个脚本并给一个报告"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -575,7 +575,7 @@ async def test_failed_execution_must_be_repaired_before_completion(settings) ->
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写脚本对比排序算法"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -662,7 +662,7 @@ async def test_unchanged_failed_command_is_blocked_until_a_repair(settings) -> N
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写脚本并运行"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -707,7 +707,7 @@ async def test_duplicate_commands_in_one_model_response_execute_once(settings) -
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写脚本并运行"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -753,7 +753,7 @@ async def test_duplicate_identical_writes_execute_once(settings) -> None:
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写脚本并运行"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -815,7 +815,7 @@ async def test_script_delivery_requires_successful_execution(settings) -> None:
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写一个 Python 脚本"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -846,7 +846,7 @@ async def test_rejected_checkpoint_does_not_spin_without_tool_calls(settings) ->
store,
max_tool_output_chars=10_000,
).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "写一个报告"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -888,7 +888,7 @@ async def test_bare_interactive_exec_is_blocked_before_registry(settings) -> Non
store,
max_tool_output_chars=10_000,
).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "你好"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -926,7 +926,7 @@ async def test_unchanged_invalid_python_write_is_not_revalidated(settings) -> No
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-light"),
spec=get_model_spec("luna"),
messages=[{"role": "user", "content": "你好"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
@@ -999,7 +999,7 @@ async def test_extreme_tier_preserves_reasoning_state_across_tool_turns(settings
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-extreme"),
spec=get_model_spec("deepseek-v4-pro"),
messages=[{"role": "user", "content": "读取 notes.txt 并检查内容"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
+8 -12
View File
@@ -1,20 +1,16 @@
from agent_platform.bootstrap import _model_payload
from agent_platform.bootstrap import LEGACY_MODEL_IDS, _model_payload
def test_bootstrap_models_override_provider_ids_and_are_public() -> None:
def test_bootstrap_models_are_agent_only_and_public() -> None:
models = _model_payload()
assert len(models) == 8
assert {model["id"] for model in models} == {"luna", "terra", "sol", "deepseek-v4-pro"}
assert all(model["base_model_id"] is None for model in models)
assert all(model["params"] == {} for model in models)
assert all(model["meta"]["toolIds"] == [] for model in models)
assert all(model["meta"]["tags"] == [{"name": "Agent"}] for model in models)
assert all(
model["access_grants"] == [{"principal_type": "user", "principal_id": "*", "permission": "read"}]
for model in models
)
chat = next(model for model in models if model["id"] == "chat-medium")
assert chat["params"]["function_calling"] == "native"
assert chat["meta"]["toolIds"] == ["server:workspace"]
assert chat["meta"]["capabilities"]["builtin_tools"] is False
work = next(model for model in models if model["id"] == "work-medium")
assert work["params"] == {}
assert work["meta"]["toolIds"] == []
assert "chat-medium" in LEGACY_MODEL_IDS
assert "work-medium" in LEGACY_MODEL_IDS
+24 -20
View File
@@ -1,23 +1,27 @@
from agent_platform.models import MODEL_SPECS, get_model_spec, openai_model_list
def test_fixed_public_model_matrix() -> None:
assert set(MODEL_SPECS) == {
"chat-light",
"chat-medium",
"chat-high",
"chat-extreme",
"work-light",
"work-medium",
"work-high",
"work-extreme",
}
assert get_model_spec("chat-light").provider_model == "ChatGPT-5.6:Luna"
assert get_model_spec("work-medium").provider_model == "ChatGPT-5.6:Terra"
assert get_model_spec("work-high").provider_model == "ChatGPT-5.6:Sol"
extreme = get_model_spec("work-extreme")
assert extreme.provider == "deepseek"
assert extreme.provider_model == "deepseek-v4-pro"
assert extreme.thinking_enabled is True
assert extreme.reasoning_effort == "max"
assert {item["id"] for item in openai_model_list()["data"]} == set(MODEL_SPECS)
def test_fixed_public_model_catalog_separates_model_and_thinking() -> None:
assert set(MODEL_SPECS) == {"luna", "terra", "sol", "deepseek-v4-pro"}
assert get_model_spec("luna").provider_model == "ChatGPT-5.6:Luna"
assert get_model_spec("terra").provider_model == "ChatGPT-5.6:Terra"
assert get_model_spec("sol").provider_model == "ChatGPT-5.6:Sol"
for model_id in ("luna", "terra", "sol"):
spec = get_model_spec(model_id)
assert spec.thinking_enabled is True
assert spec.thinking_adjustable is False
assert spec.thinking_label == "开启(不分档)"
assert spec.reasoning_effort is None
deepseek = get_model_spec("deepseek-v4-pro")
assert deepseek.provider == "deepseek"
assert deepseek.provider_model == "deepseek-v4-pro"
assert deepseek.thinking_enabled is True
assert deepseek.thinking_adjustable is False
assert deepseek.thinking_label == "极高"
assert deepseek.reasoning_effort == "max"
public_models = openai_model_list()["data"]
assert {item["id"] for item in public_models} == set(MODEL_SPECS)
assert all("k1412" in item for item in public_models)
+44 -97
View File
@@ -18,7 +18,7 @@ class FakeModelProvider:
async def complete(self, **kwargs):
self.completed.append(kwargs)
return {
"choices": [{"message": {"role": "assistant", "content": "work complete"}, "finish_reason": "stop"}],
"choices": [{"message": {"role": "assistant", "content": "agent complete"}, "finish_reason": "stop"}],
"usage": {"total_tokens": 10},
}
@@ -26,25 +26,13 @@ class FakeModelProvider:
self.forwarded.append(payload)
self.forward_options.append(kwargs)
request = httpx.Request("POST", "https://model.example.test/v1/chat/completions")
if payload.get("stream"):
return httpx.Response(
200,
request=request,
headers={"content-type": "text/event-stream"},
content=(
'data: {"model":"ChatGPT-5.6:Luna","choices":[{"index":0,"delta":'
'{"tool_calls":[{"index":0,"id":"call-1","type":"function","function":'
'{"name":"read_file","arguments":"{}"}}]},"finish_reason":null}]}\n\n'
"data: [DONE]\n\n"
),
)
return httpx.Response(
200,
request=request,
headers={"content-type": "application/json"},
json={
"model": payload["model"],
"choices": [{"message": {"role": "assistant", "content": "chat reply"}, "finish_reason": "stop"}],
"choices": [{"message": {"role": "assistant", "content": "background reply"}}],
},
)
@@ -71,135 +59,94 @@ def headers(settings, identity_jwt, chat_id="chat-1"):
}
def test_model_list_requires_internal_key(settings) -> None:
app = create_app(
def _app(settings, provider=None):
return create_app(
settings,
store=RuntimeStore(settings.database_url),
provider=FakeModelProvider(),
provider=provider or FakeModelProvider(),
tools=FakeTools(),
)
with TestClient(app) as client:
def test_model_list_requires_internal_key_and_exposes_four_models(settings) -> None:
with TestClient(_app(settings)) as client:
assert client.get("/v1/models").status_code == 401
response = client.get(
"/v1/models",
headers={"Authorization": f"Bearer {settings.internal_provider_key}"},
)
assert response.status_code == 200
assert len(response.json()["data"]) == 8
def test_chat_maps_provider_model_and_work_upgrade_is_one_way(settings, identity_jwt) -> None:
provider = FakeModelProvider()
app = create_app(settings, store=RuntimeStore(settings.database_url), provider=provider, tools=FakeTools())
with TestClient(app) as client:
chat = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt),
json={"model": "chat-light", "messages": [{"role": "user", "content": "hello"}]},
)
assert chat.status_code == 200
assert provider.forwarded[0]["model"] == "ChatGPT-5.6:Luna"
assert chat.json()["model"] == "chat-light"
extreme_chat = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "extreme-chat"),
json={"model": "chat-extreme", "messages": [{"role": "user", "content": "think"}]},
)
assert extreme_chat.status_code == 200
assert provider.forwarded[-1]["model"] == "deepseek-v4-pro"
assert provider.forward_options[-1] == {
"provider": "deepseek",
"thinking_enabled": True,
"reasoning_effort": "max",
"max_output_tokens": 16_384,
assert {item["id"] for item in response.json()["data"]} == {
"luna",
"terra",
"sol",
"deepseek-v4-pro",
}
work = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt),
json={"model": "work-high", "messages": [{"role": "user", "content": "do it"}]},
)
assert work.status_code == 200
assert provider.completed[-1]["model"] == "ChatGPT-5.6:Sol"
extreme_work = client.post(
def test_every_user_model_runs_the_custom_agent_loop(settings, identity_jwt) -> None:
provider = FakeModelProvider()
with TestClient(_app(settings, provider)) as client:
luna = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "extreme-work"),
json={"model": "work-extreme", "messages": [{"role": "user", "content": "do it carefully"}]},
headers=headers(settings, identity_jwt, "luna-run"),
json={"model": "luna", "messages": [{"role": "user", "content": "hello"}]},
)
assert extreme_work.status_code == 200
assert luna.status_code == 200
assert luna.json()["model"] == "luna"
assert provider.completed[-1]["model"] == "ChatGPT-5.6:Luna"
assert provider.completed[-1]["thinking_enabled"] is True
assert provider.forwarded == []
deepseek = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "deepseek-run"),
json={"model": "deepseek-v4-pro", "messages": [{"role": "user", "content": "do it"}]},
)
assert deepseek.status_code == 200
assert provider.completed[-1]["model"] == "deepseek-v4-pro"
assert provider.completed[-1]["provider"] == "deepseek"
assert provider.completed[-1]["reasoning_effort"] == "max"
downgrade = client.post(
legacy = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt),
json={"model": "chat-medium", "messages": [{"role": "user", "content": "back"}]},
headers=headers(settings, identity_jwt, "legacy-run"),
json={"model": "work-medium", "messages": [{"role": "user", "content": "old"}]},
)
assert downgrade.status_code == 409
assert legacy.status_code == 404
def test_chat_stream_hides_provider_model_and_preserves_native_tool_calls(settings, identity_jwt) -> None:
def test_openwebui_background_task_bypasses_agent_loop(settings, identity_jwt) -> None:
provider = FakeModelProvider()
app = create_app(settings, store=RuntimeStore(settings.database_url), provider=provider, tools=FakeTools())
with TestClient(app) as client:
response = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "chat-stream"),
json={
"model": "chat-light",
"stream": True,
"messages": [{"role": "user", "content": "inspect"}],
},
)
assert response.status_code == 200
assert '"model": "chat-light"' in response.text
assert "ChatGPT-5.6:Luna" not in response.text
assert '"tool_calls"' in response.text
assert '"name": "read_file"' in response.text
assert "data: [DONE]" in response.text
def test_openwebui_background_task_bypasses_work_loop(settings, identity_jwt) -> None:
provider = FakeModelProvider()
app = create_app(settings, store=RuntimeStore(settings.database_url), provider=provider, tools=FakeTools())
with TestClient(app) as client:
with TestClient(_app(settings, provider)) as client:
response = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "background-task"),
json={
"model": "work-medium",
"model": "terra",
"messages": [{"role": "user", "content": "Generate tags"}],
"metadata": {"task": "tags_generation"},
},
)
assert response.status_code == 200
assert response.json()["model"] == "work-medium"
assert response.json()["model"] == "terra"
assert provider.completed == []
assert provider.forwarded[-1]["model"] == "ChatGPT-5.6:Terra"
def test_work_stream_never_emits_delta_tool_calls(settings, identity_jwt) -> None:
app = create_app(
settings,
store=RuntimeStore(settings.database_url),
provider=FakeModelProvider(),
tools=FakeTools(),
)
with TestClient(app) as client:
def test_agent_stream_never_emits_delta_tool_calls(settings, identity_jwt) -> None:
with TestClient(_app(settings)) as client:
response = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "stream-chat"),
json={
"model": "work-medium",
"model": "terra",
"stream": True,
"messages": [{"role": "user", "content": "do it"}],
},
)
assert response.status_code == 200
assert "work complete" in response.text
assert "agent complete" in response.text
for line in response.text.splitlines():
if line.startswith("data: {"):
chunk = json.loads(line.removeprefix("data: "))
-12
View File
@@ -3,18 +3,6 @@ from __future__ import annotations
from agent_platform.store import RuntimeStore
async def test_conversation_mode_upgrade_is_one_way(settings) -> None:
store = RuntimeStore(settings.database_url)
await store.initialize()
try:
assert await store.select_mode("u1", "c1", "chat") == "chat"
assert await store.select_mode("u1", "c1", "work") == "work"
assert await store.select_mode("u1", "c1", "chat") == "work"
assert await store.select_mode("u2", "c1", "chat") == "chat"
finally:
await store.close()
async def test_memory_and_events_are_user_scoped(settings) -> None:
store = RuntimeStore(settings.database_url)
await store.initialize()