refactor: use a single agent model path
This commit is contained in:
@@ -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
@@ -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 @@
|
||||
"""Model gateway and independently evolvable Work Agent runtime."""
|
||||
"""Model gateway and independently evolvable K1412 Agent runtime."""
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user