feat: add DeepSeek extreme reasoning tier

This commit is contained in:
wuyang
2026-07-26 17:40:51 +08:00
parent ade5ea1210
commit 2e8e81c790
25 changed files with 567 additions and 30 deletions
+4
View File
@@ -14,6 +14,10 @@ INTERNAL_GATEWAY_KEY=
MODEL_API_KEY= MODEL_API_KEY=
MODEL_API_BASE_URL=https://api.k1412.top MODEL_API_BASE_URL=https://api.k1412.top
# Extreme tier provider. Keep the key only in the protected server-side .env.
DEEPSEEK_API_KEY=
DEEPSEEK_API_BASE_URL=https://api.deepseek.com
# Storage # Storage
POSTGRES_USER=agent POSTGRES_USER=agent
POSTGRES_PASSWORD= POSTGRES_PASSWORD=
+16 -1
View File
@@ -32,7 +32,7 @@ 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 Open WebUI is pinned and lightly patched. Its model picker is replaced by two
product-level choices: `Chat / Work` and `轻度 / 中 / 高`. Provider URLs, product-level choices: `Chat / Work` and `轻度 / 中 / 高 / 极高`. Provider URLs,
provider model IDs, API keys, tool server settings, system prompts, and runtime provider model IDs, API keys, tool server settings, system prompts, and runtime
parameters remain server-side. parameters remain server-side.
@@ -43,6 +43,7 @@ The inference mapping is fixed:
| 轻度 | `ChatGPT-5.6:Luna` | | 轻度 | `ChatGPT-5.6:Luna` |
| 中 | `ChatGPT-5.6:Terra` | | 中 | `ChatGPT-5.6:Terra` |
| 高 | `ChatGPT-5.6:Sol` | | 高 | `ChatGPT-5.6:Sol` |
| 极高 | `deepseek-v4-pro`Thinking`reasoning_effort=max` |
## Local development ## Local development
@@ -82,6 +83,20 @@ test, with:
./scripts/verify.sh ./scripts/verify.sh
``` ```
Run a live Work 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
```
The evaluator reports latency, token usage, tool counts, completion evidence,
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 Run the disposable six-service integration stack with a deterministic fake
model provider and exercise registration approval, both Agent loops, workspace model provider and exercise registration approval, both Agent loops, workspace
isolation, and the one-way mode upgrade with: isolation, and the one-way mode upgrade with:
+1 -1
View File
@@ -158,7 +158,7 @@ async def bootstrap() -> None:
) )
model_response.raise_for_status() model_response.raise_for_status()
print("Open WebUI bootstrap complete: auth policy, workspace tools, and six fixed Agent entries are ready.") print("Open WebUI bootstrap complete: auth policy, workspace tools, and eight fixed Agent entries are ready.")
def run() -> None: def run() -> None:
+5
View File
@@ -26,6 +26,8 @@ def _float(name: str, default: float) -> float:
class Settings: class Settings:
model_api_base_url: str model_api_base_url: str
model_api_key: str model_api_key: str
deepseek_api_base_url: str
deepseek_api_key: str
openwebui_forward_jwt_secret: str openwebui_forward_jwt_secret: str
internal_provider_key: str internal_provider_key: str
internal_gateway_key: str internal_gateway_key: str
@@ -49,6 +51,8 @@ class Settings:
return cls( return cls(
model_api_base_url=os.getenv("MODEL_API_BASE_URL", "https://api.k1412.top").rstrip("/"), model_api_base_url=os.getenv("MODEL_API_BASE_URL", "https://api.k1412.top").rstrip("/"),
model_api_key=os.getenv("MODEL_API_KEY", ""), model_api_key=os.getenv("MODEL_API_KEY", ""),
deepseek_api_base_url=os.getenv("DEEPSEEK_API_BASE_URL", "https://api.deepseek.com").rstrip("/"),
deepseek_api_key=os.getenv("DEEPSEEK_API_KEY", ""),
openwebui_forward_jwt_secret=os.getenv("OPENWEBUI_FORWARD_JWT_SECRET", ""), openwebui_forward_jwt_secret=os.getenv("OPENWEBUI_FORWARD_JWT_SECRET", ""),
internal_provider_key=os.getenv("INTERNAL_PROVIDER_KEY", ""), internal_provider_key=os.getenv("INTERNAL_PROVIDER_KEY", ""),
internal_gateway_key=os.getenv("INTERNAL_GATEWAY_KEY", ""), internal_gateway_key=os.getenv("INTERNAL_GATEWAY_KEY", ""),
@@ -73,6 +77,7 @@ class Settings:
name name
for name, value in ( for name, value in (
("MODEL_API_KEY", self.model_api_key), ("MODEL_API_KEY", self.model_api_key),
("DEEPSEEK_API_KEY", self.deepseek_api_key),
("OPENWEBUI_FORWARD_JWT_SECRET", self.openwebui_forward_jwt_secret), ("OPENWEBUI_FORWARD_JWT_SECRET", self.openwebui_forward_jwt_secret),
("INTERNAL_PROVIDER_KEY", self.internal_provider_key), ("INTERNAL_PROVIDER_KEY", self.internal_provider_key),
("INTERNAL_GATEWAY_KEY", self.internal_gateway_key), ("INTERNAL_GATEWAY_KEY", self.internal_gateway_key),
+25 -6
View File
@@ -4,7 +4,8 @@ from dataclasses import dataclass
from typing import Literal from typing import Literal
Mode = Literal["chat", "work"] Mode = Literal["chat", "work"]
Strength = Literal["light", "medium", "high"] Strength = Literal["light", "medium", "high", "extreme"]
Provider = Literal["k1412", "deepseek"]
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -13,15 +14,20 @@ class ModelSpec:
display_name: str display_name: str
mode: Mode mode: Mode
strength: Strength strength: Strength
provider: Provider
provider_model: str provider_model: str
thinking_enabled: bool
reasoning_effort: str | None
max_output_tokens: int
max_iterations: int max_iterations: int
context_char_budget: int context_char_budget: int
_TIERS = { _TIERS = {
"light": ("轻度", "ChatGPT-5.6:Luna", 8, 120_000), "light": ("轻度", "k1412", "ChatGPT-5.6:Luna", False, None, 4_096, 8, 120_000),
"medium": ("", "ChatGPT-5.6:Terra", 16, 240_000), "medium": ("", "k1412", "ChatGPT-5.6:Terra", False, None, 4_096, 16, 240_000),
"high": ("", "ChatGPT-5.6:Sol", 24, 400_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] = { MODEL_SPECS: dict[str, ModelSpec] = {
@@ -30,12 +36,25 @@ MODEL_SPECS: dict[str, ModelSpec] = {
display_name=f"{'Chat' if mode == 'chat' else 'Work'} · {label}", display_name=f"{'Chat' if mode == 'chat' else 'Work'} · {label}",
mode=mode, mode=mode,
strength=strength, strength=strength,
provider_model=provider, provider=provider,
provider_model=provider_model,
thinking_enabled=thinking_enabled,
reasoning_effort=reasoning_effort,
max_output_tokens=max_output_tokens,
max_iterations=max_iterations, max_iterations=max_iterations,
context_char_budget=context_budget, context_char_budget=context_budget,
) )
for mode in ("chat", "work") for mode in ("chat", "work")
for strength, (label, provider, max_iterations, context_budget) in _TIERS.items() for strength, (
label,
provider,
provider_model,
thinking_enabled,
reasoning_effort,
max_output_tokens,
max_iterations,
context_budget,
) in _TIERS.items()
} }
+7 -1
View File
@@ -160,7 +160,13 @@ def create_app(
async def forward_direct(): async def forward_direct():
payload = body.model_dump(exclude_none=True) payload = body.model_dump(exclude_none=True)
payload["model"] = spec.provider_model payload["model"] = spec.provider_model
response = await app.state.provider.forward(payload) response = await app.state.provider.forward(
payload,
provider=spec.provider,
thinking_enabled=spec.thinking_enabled,
reasoning_effort=spec.reasoning_effort,
max_output_tokens=spec.max_output_tokens,
)
if body.stream: if body.stream:
passthrough_headers = { passthrough_headers = {
key: value key: value
+19 -2
View File
@@ -599,6 +599,10 @@ class AgentLoop:
model=spec.provider_model, model=spec.provider_model,
messages=messages, messages=messages,
tools=available_specs, tools=available_specs,
provider=spec.provider,
thinking_enabled=spec.thinking_enabled,
reasoning_effort=spec.reasoning_effort,
max_output_tokens=spec.max_output_tokens,
) )
choice = response["choices"][0] choice = response["choices"][0]
message = choice.get("message") or {} message = choice.get("message") or {}
@@ -648,9 +652,12 @@ class AgentLoop:
"completion.rejected", "completion.rejected",
{"reason": failure, "iteration": iteration + 1, "depth": depth}, {"reason": failure, "iteration": iteration + 1, "depth": depth},
) )
assistant_checkpoint = {"role": "assistant", "content": candidate}
if isinstance(message.get("reasoning_content"), str):
assistant_checkpoint["reasoning_content"] = message["reasoning_content"]
messages.extend( messages.extend(
[ [
{"role": "assistant", "content": candidate}, assistant_checkpoint,
{"role": "user", "content": recovery_message(failure)}, {"role": "user", "content": recovery_message(failure)},
] ]
) )
@@ -683,6 +690,8 @@ class AgentLoop:
"content": message.get("content"), "content": message.get("content"),
"tool_calls": tool_calls, "tool_calls": tool_calls,
} }
if isinstance(message.get("reasoning_content"), str):
assistant_message["reasoning_content"] = message["reasoning_content"]
messages.append(assistant_message) messages.append(assistant_message)
blocked_call_reasons: dict[str, str] = {} blocked_call_reasons: dict[str, str] = {}
batch_mutation_epoch = 0 batch_mutation_epoch = 0
@@ -801,7 +810,15 @@ class AgentLoop:
), ),
} }
) )
response = await self.provider.complete(model=spec.provider_model, messages=messages, tools=None) response = await self.provider.complete(
model=spec.provider_model,
messages=messages,
tools=None,
provider=spec.provider,
thinking_enabled=spec.thinking_enabled,
reasoning_effort=spec.reasoning_effort,
max_output_tokens=spec.max_output_tokens,
)
answer = str(response["choices"][0].get("message", {}).get("content") or "").strip() answer = str(response["choices"][0].get("message", {}).get("content") or "").strip()
if completion_failure( if completion_failure(
artifact_required, artifact_required,
+63 -12
View File
@@ -6,6 +6,7 @@ from typing import Any
import httpx import httpx
from agent_platform.config import Settings from agent_platform.config import Settings
from agent_platform.models import Provider
TRANSIENT_COMPLETE_STATUS_CODES = frozenset({408, 425, 429, 500, 502, 503, 504, 524}) TRANSIENT_COMPLETE_STATUS_CODES = frozenset({408, 425, 429, 500, 502, 503, 504, 524})
COMPLETE_MAX_ATTEMPTS = 2 COMPLETE_MAX_ATTEMPTS = 2
@@ -132,11 +133,37 @@ class ModelProvider:
@property @property
def headers(self) -> dict[str, str]: def headers(self) -> dict[str, str]:
return self._headers("k1412")
def _headers(self, provider: Provider) -> dict[str, str]:
key = self.settings.deepseek_api_key if provider == "deepseek" else self.settings.model_api_key
return { return {
"Authorization": f"Bearer {self.settings.model_api_key}", "Authorization": f"Bearer {key}",
"Content-Type": "application/json", "Content-Type": "application/json",
} }
def _base_url(self, provider: Provider) -> str:
return self.settings.deepseek_api_base_url if provider == "deepseek" else self.settings.model_api_base_url
@staticmethod
def _provider_payload(
payload: dict[str, Any],
*,
provider: Provider,
thinking_enabled: bool,
reasoning_effort: str | None,
max_output_tokens: int,
) -> dict[str, Any]:
value = dict(payload)
value.pop("thinking", None)
value.pop("reasoning_effort", None)
value["max_tokens"] = max_output_tokens
if provider == "deepseek" and thinking_enabled:
value["thinking"] = {"type": "enabled"}
if reasoning_effort:
value["reasoning_effort"] = reasoning_effort
return value
async def close(self) -> None: async def close(self) -> None:
if self._owns_client: if self._owns_client:
await self.client.aclose() await self.client.aclose()
@@ -148,13 +175,22 @@ class ModelProvider:
messages: list[dict[str, Any]], messages: list[dict[str, Any]],
tools: list[dict[str, Any]] | None = None, tools: list[dict[str, Any]] | None = None,
temperature: float | None = None, temperature: float | None = None,
provider: Provider = "k1412",
thinking_enabled: bool = False,
reasoning_effort: str | None = None,
max_output_tokens: int = COMPLETE_MAX_TOKENS,
) -> dict[str, Any]: ) -> dict[str, Any]:
payload: dict[str, Any] = { payload = self._provider_payload(
"model": model, {
"messages": messages, "model": model,
"stream": True, "messages": messages,
"max_tokens": COMPLETE_MAX_TOKENS, "stream": True,
} },
provider=provider,
thinking_enabled=thinking_enabled,
reasoning_effort=reasoning_effort,
max_output_tokens=max_output_tokens,
)
if tools: if tools:
payload["tools"] = tools payload["tools"] = tools
payload["tool_choice"] = "auto" payload["tool_choice"] = "auto"
@@ -165,8 +201,8 @@ class ModelProvider:
try: try:
async with self.client.stream( async with self.client.stream(
"POST", "POST",
completions_url(self.settings.model_api_base_url), completions_url(self._base_url(provider)),
headers=self.headers, headers=self._headers(provider),
json=payload, json=payload,
) as response: ) as response:
if response.status_code in TRANSIENT_COMPLETE_STATUS_CODES and attempt < COMPLETE_MAX_ATTEMPTS: if response.status_code in TRANSIENT_COMPLETE_STATUS_CODES and attempt < COMPLETE_MAX_ATTEMPTS:
@@ -184,11 +220,26 @@ class ModelProvider:
raise RuntimeError("Model provider retry loop exited unexpectedly") raise RuntimeError("Model provider retry loop exited unexpectedly")
async def forward(self, payload: dict[str, Any]) -> httpx.Response: async def forward(
self,
payload: dict[str, Any],
*,
provider: Provider = "k1412",
thinking_enabled: bool = False,
reasoning_effort: str | None = None,
max_output_tokens: int = COMPLETE_MAX_TOKENS,
) -> httpx.Response:
payload = self._provider_payload(
payload,
provider=provider,
thinking_enabled=thinking_enabled,
reasoning_effort=reasoning_effort,
max_output_tokens=max_output_tokens,
)
request = self.client.build_request( request = self.client.build_request(
"POST", "POST",
completions_url(self.settings.model_api_base_url), completions_url(self._base_url(provider)),
headers=self.headers, headers=self._headers(provider),
json=payload, json=payload,
) )
response = await self.client.send(request, stream=bool(payload.get("stream"))) response = await self.client.send(request, stream=bool(payload.get("stream")))
+1
View File
@@ -17,6 +17,7 @@ services:
runtime: runtime:
environment: environment:
MODEL_API_BASE_URL: http://stub-provider:9000 MODEL_API_BASE_URL: http://stub-provider:9000
DEEPSEEK_API_BASE_URL: http://stub-provider:9000
depends_on: depends_on:
stub-provider: stub-provider:
condition: service_healthy condition: service_healthy
+2
View File
@@ -123,6 +123,8 @@ services:
environment: environment:
MODEL_API_BASE_URL: ${MODEL_API_BASE_URL:-https://api.k1412.top} MODEL_API_BASE_URL: ${MODEL_API_BASE_URL:-https://api.k1412.top}
MODEL_API_KEY: ${MODEL_API_KEY:?MODEL_API_KEY is required} MODEL_API_KEY: ${MODEL_API_KEY:?MODEL_API_KEY is required}
DEEPSEEK_API_BASE_URL: ${DEEPSEEK_API_BASE_URL:-https://api.deepseek.com}
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:?DEEPSEEK_API_KEY is required}
OPENWEBUI_FORWARD_JWT_SECRET: ${OPENWEBUI_FORWARD_JWT_SECRET:?OPENWEBUI_FORWARD_JWT_SECRET is required} OPENWEBUI_FORWARD_JWT_SECRET: ${OPENWEBUI_FORWARD_JWT_SECRET:?OPENWEBUI_FORWARD_JWT_SECRET is required}
INTERNAL_PROVIDER_KEY: ${INTERNAL_PROVIDER_KEY:?INTERNAL_PROVIDER_KEY is required} INTERNAL_PROVIDER_KEY: ${INTERNAL_PROVIDER_KEY:?INTERNAL_PROVIDER_KEY is required}
INTERNAL_GATEWAY_KEY: ${INTERNAL_GATEWAY_KEY:?INTERNAL_GATEWAY_KEY is required} INTERNAL_GATEWAY_KEY: ${INTERNAL_GATEWAY_KEY:?INTERNAL_GATEWAY_KEY is required}
+2
View File
@@ -123,6 +123,8 @@ services:
environment: environment:
MODEL_API_BASE_URL: ${MODEL_API_BASE_URL:-https://api.k1412.top} MODEL_API_BASE_URL: ${MODEL_API_BASE_URL:-https://api.k1412.top}
MODEL_API_KEY: ${MODEL_API_KEY:?MODEL_API_KEY is required} MODEL_API_KEY: ${MODEL_API_KEY:?MODEL_API_KEY is required}
DEEPSEEK_API_BASE_URL: ${DEEPSEEK_API_BASE_URL:-https://api.deepseek.com}
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:?DEEPSEEK_API_KEY is required}
OPENWEBUI_FORWARD_JWT_SECRET: ${OPENWEBUI_FORWARD_JWT_SECRET:?OPENWEBUI_FORWARD_JWT_SECRET is required} OPENWEBUI_FORWARD_JWT_SECRET: ${OPENWEBUI_FORWARD_JWT_SECRET:?OPENWEBUI_FORWARD_JWT_SECRET is required}
INTERNAL_PROVIDER_KEY: ${INTERNAL_PROVIDER_KEY:?INTERNAL_PROVIDER_KEY is required} INTERNAL_PROVIDER_KEY: ${INTERNAL_PROVIDER_KEY:?INTERNAL_PROVIDER_KEY is required}
INTERNAL_GATEWAY_KEY: ${INTERNAL_GATEWAY_KEY:?INTERNAL_GATEWAY_KEY is required} INTERNAL_GATEWAY_KEY: ${INTERNAL_GATEWAY_KEY:?INTERNAL_GATEWAY_KEY is required}
+5 -2
View File
@@ -10,16 +10,19 @@
const strengths = [ const strengths = [
{ key: 'light', label: '轻度' }, { key: 'light', label: '轻度' },
{ key: 'medium', label: '中' }, { key: 'medium', label: '中' },
{ key: 'high', label: '高' } { key: 'high', label: '高' },
{ key: 'extreme', label: '极高' }
]; ];
const allowed = new Set([ const allowed = new Set([
'chat-light', 'chat-light',
'chat-medium', 'chat-medium',
'chat-high', 'chat-high',
'chat-extreme',
'work-light', 'work-light',
'work-medium', 'work-medium',
'work-high' 'work-high',
'work-extreme'
]); ]);
$: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'chat-medium'; $: selected = allowed.has(selectedModels?.[0]) ? selectedModels[0] : 'chat-medium';
+7
View File
@@ -34,6 +34,13 @@ Every run records its model tier, strategy version, scheduler version, context
policy, model/tool events, failures, and completion. This event stream is the policy, model/tool events, failures, and completion. This event stream is the
foundation for replay, evaluation, and future experiment assignment. 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.
Work completion is evidence-gated. Requests for scripts, reports, code, or Work completion is evidence-gated. Requests for scripts, reports, code, or
other concrete artifacts are not accepted as complete until a file mutation 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 has succeeded and a later read, diff, or command has verified the latest
+5
View File
@@ -39,6 +39,11 @@ listed above. `https://api.k1412.top` remains the external API entrypoint, but
Runtime must not send long-running Agent inference through the public reverse Runtime must not send long-running Agent inference through the public reverse
proxy. 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,
Compose file, image, repository, or log.
The SSH override removes `/var/run/docker.sock` from Gateway and mounts the The SSH override removes `/var/run/docker.sock` from Gateway and mounts the
dedicated SSH directory read-only at `/root/.ssh`. dedicated SSH directory read-only at `/root/.ssh`.
+3 -1
View File
@@ -29,9 +29,11 @@ EXPECTED_MODELS = {
"chat-light", "chat-light",
"chat-medium", "chat-medium",
"chat-high", "chat-high",
"chat-extreme",
"work-light", "work-light",
"work-medium", "work-medium",
"work-high", "work-high",
"work-extreme",
} }
@@ -286,7 +288,7 @@ def main() -> None:
assert downgrade.status_code == 409, downgrade.text assert downgrade.status_code == 409, downgrade.text
print( print(
"E2E passed: auth approval, six models, Chat tools, Work evidence, file downloads, " "E2E passed: auth approval, eight models, Chat tools, Work evidence, file downloads, "
"isolation, and downgrade lock." "isolation, and downgrade lock."
) )
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import asyncio
import json
import os
import tempfile
import time
import uuid
from dataclasses import replace
from pathlib import Path
from typing import Any
from docker.errors import NotFound
from agent_platform.auth import UserIdentity
from agent_platform.config import Settings
from agent_platform.gateway.provider import DockerExecutionProvider, workspace_ref
from agent_platform.models import get_model_spec
from agent_platform.runtime.loop import AgentLoop
from agent_platform.runtime.provider import ModelProvider
from agent_platform.runtime.schemas import PlanItem
from agent_platform.runtime.tools import TOOL_METADATA, ToolContext
from agent_platform.store import RuntimeStore
EVAL_TOOL_NAMES = (
"workspace_status",
"list_files",
"read_file",
"search_files",
"write_file",
"apply_patch",
"exec",
"update_plan",
)
class DirectDockerRegistry:
def __init__(
self,
execution: DockerExecutionProvider,
store: RuntimeStore,
tool_timeout_seconds: int,
) -> None:
self.execution = execution
self.store = store
self.tool_timeout_seconds = tool_timeout_seconds
def specs(self, *, read_only: bool = False, allow_delegate: bool = True) -> list[dict[str, Any]]:
names = [name for name in EVAL_TOOL_NAMES if not read_only or TOOL_METADATA[name].read_only]
return [TOOL_METADATA[name].openai_spec() for name in names]
async def execute(
self,
name: str,
arguments: dict[str, Any],
context: ToolContext,
) -> dict[str, Any]:
user_id = context.identity.user_id
if name == "workspace_status":
result = await self.execution.status(user_id)
elif name == "list_files":
result = await self.execution.list_files(
user_id,
str(arguments.get("path", ".")),
int(arguments.get("max_depth", 4)),
int(arguments.get("limit", 500)),
)
elif name == "read_file":
result = await self.execution.read_file(
user_id,
str(arguments["path"]),
int(arguments.get("start_line", 1)),
int(arguments.get("max_lines", 1000)),
)
elif name == "search_files":
result = await self.execution.search_files(
user_id,
str(arguments["query"]),
str(arguments.get("path", ".")),
arguments.get("glob"),
int(arguments.get("limit", 200)),
)
elif name == "write_file":
result = await self.execution.write_file(
user_id,
str(arguments["path"]),
str(arguments["content"]),
)
elif name == "apply_patch":
result = await self.execution.apply_patch(
user_id,
str(arguments["patch"]),
str(arguments.get("cwd", ".")),
)
elif name == "exec":
result = await self.execution.exec(
user_id,
str(arguments["command"]),
str(arguments.get("cwd", ".")),
min(
int(arguments.get("timeout_seconds", 120)),
self.tool_timeout_seconds,
),
)
elif name == "update_plan":
items = [PlanItem.model_validate(item).model_dump() for item in arguments.get("items", [])]
await self.store.update_plan(user_id, context.chat_id, items)
return {
"ok": True,
"explanation": arguments.get("explanation"),
"items": items,
}
else:
raise ValueError(f"Unsupported evaluation tool: {name}")
return result.model_dump()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the real Work loop against a live provider in a disposable Docker workspace."
)
parser.add_argument("--model", default="work-extreme")
parser.add_argument(
"--prompt",
default="写脚本对比一下几个常见排序算法,给一个报告",
)
parser.add_argument(
"--workspace-image",
default=os.getenv("WORKSPACE_IMAGE", "k1412-agent-workspace:test"),
)
parser.add_argument("--keep-workspace", action="store_true")
return parser.parse_args()
async def cleanup_workspace(execution: DockerExecutionProvider, user_id: str) -> None:
ref = workspace_ref(user_id)
try:
execution.client.containers.get(ref.container_name).remove(force=True)
except NotFound:
pass
try:
execution.client.volumes.get(ref.volume_name).remove(force=True)
except NotFound:
pass
try:
execution.client.networks.get(ref.network_name).remove()
except NotFound:
pass
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")
user_id = f"live-eval-{uuid.uuid4().hex}"
chat_id = f"live-eval-{uuid.uuid4().hex}"
started = time.monotonic()
event_counts: dict[str, int] = {}
usage_totals = {
"prompt_tokens": 0,
"completion_tokens": 0,
"reasoning_tokens": 0,
"cached_tokens": 0,
"total_tokens": 0,
}
with tempfile.TemporaryDirectory(prefix="k1412-live-eval-") as temp_dir:
settings = replace(
Settings.from_env(),
database_url=f"sqlite+aiosqlite:///{Path(temp_dir) / 'runtime.sqlite3'}",
workspace_image=args.workspace_image,
workspace_network_enabled=False,
workspace_memory_limit="1g",
workspace_cpu_limit=1.0,
workspace_pids_limit=128,
tool_timeout_seconds=180,
)
store = RuntimeStore(settings.database_url)
await store.initialize()
provider = ModelProvider(settings)
execution = DockerExecutionProvider(settings)
registry = DirectDockerRegistry(execution, store, settings.tool_timeout_seconds)
async def record(event_type: str, payload: dict[str, Any]) -> None:
event_counts[event_type] = event_counts.get(event_type, 0) + 1
if event_type == "model.responded":
usage = payload.get("usage") or {}
prompt_details = usage.get("prompt_tokens_details") or {}
completion_details = usage.get("completion_tokens_details") or {}
usage_totals["prompt_tokens"] += int(usage.get("prompt_tokens", 0))
usage_totals["completion_tokens"] += int(usage.get("completion_tokens", 0))
usage_totals["reasoning_tokens"] += int(completion_details.get("reasoning_tokens", 0))
usage_totals["cached_tokens"] += int(
prompt_details.get("cached_tokens", 0) or usage.get("prompt_cache_hit_tokens", 0)
)
usage_totals["total_tokens"] += int(usage.get("total_tokens", 0))
if event_type == "tool.completed":
print(
json.dumps(
{
"event": event_type,
"name": payload.get("name"),
"ok": payload.get("ok"),
"summary": str(payload.get("summary", ""))[:500],
},
ensure_ascii=False,
),
flush=True,
)
try:
answer = await AgentLoop(
provider,
registry,
store,
max_tool_output_chars=settings.max_tool_output_chars,
).run(
spec=spec,
messages=[{"role": "user", "content": args.prompt}],
identity=UserIdentity(user_id, "", "Live Eval", "user"),
raw_user_jwt="local-evaluation",
chat_id=chat_id,
callback=record,
)
listing = await execution.list_files(user_id, ".", 4, 500)
print(
json.dumps(
{
"model": spec.public_id,
"provider_model": spec.provider_model,
"elapsed_seconds": round(time.monotonic() - started, 3),
"events": event_counts,
"usage": usage_totals,
"answer": answer,
"workspace": listing.model_dump(),
"workspace_id": workspace_ref(user_id).workspace_id if args.keep_workspace else None,
},
ensure_ascii=False,
indent=2,
)
)
finally:
await provider.close()
await store.close()
if not args.keep_workspace:
await cleanup_workspace(execution, user_id)
execution.client.close()
if __name__ == "__main__":
asyncio.run(main())
+1
View File
@@ -18,6 +18,7 @@ export OPENWEBUI_FORWARD_JWT_SECRET="e2e-forward-jwt-secret-at-least-32-bytes"
export INTERNAL_PROVIDER_KEY="e2e-provider-secret-at-least-32-bytes" export INTERNAL_PROVIDER_KEY="e2e-provider-secret-at-least-32-bytes"
export INTERNAL_GATEWAY_KEY="e2e-gateway-secret-at-least-32-bytes" export INTERNAL_GATEWAY_KEY="e2e-gateway-secret-at-least-32-bytes"
export MODEL_API_KEY="e2e-model-key" export MODEL_API_KEY="e2e-model-key"
export DEEPSEEK_API_KEY="e2e-deepseek-key"
export POSTGRES_PASSWORD="e2e-postgres-password" export POSTGRES_PASSWORD="e2e-postgres-password"
export WEB_IMAGE="k1412-agent-web:e2e" export WEB_IMAGE="k1412-agent-web:e2e"
export RUNTIME_IMAGE="k1412-agent-runtime:e2e" export RUNTIME_IMAGE="k1412-agent-runtime:e2e"
+1
View File
@@ -24,6 +24,7 @@ export OPENWEBUI_FORWARD_JWT_SECRET=verify-forward-jwt-secret-at-least-32-bytes
export INTERNAL_PROVIDER_KEY=verify-provider-secret-at-least-32-bytes export INTERNAL_PROVIDER_KEY=verify-provider-secret-at-least-32-bytes
export INTERNAL_GATEWAY_KEY=verify-gateway-secret-at-least-32-bytes export INTERNAL_GATEWAY_KEY=verify-gateway-secret-at-least-32-bytes
export MODEL_API_KEY=verify-model-key export MODEL_API_KEY=verify-model-key
export DEEPSEEK_API_KEY=verify-deepseek-key
export POSTGRES_PASSWORD=verify-postgres-password export POSTGRES_PASSWORD=verify-postgres-password
export WEB_IMAGE=docker.k1412.top/wuyang/k1412-agent-web:verify export WEB_IMAGE=docker.k1412.top/wuyang/k1412-agent-web:verify
export RUNTIME_IMAGE=docker.k1412.top/wuyang/k1412-agent-runtime:verify export RUNTIME_IMAGE=docker.k1412.top/wuyang/k1412-agent-runtime:verify
+2
View File
@@ -13,6 +13,8 @@ def settings(tmp_path: Path) -> Settings:
return Settings( return Settings(
model_api_base_url="https://model.example.test", model_api_base_url="https://model.example.test",
model_api_key="model-secret", model_api_key="model-secret",
deepseek_api_base_url="https://api.deepseek.example.test",
deepseek_api_key="deepseek-secret",
openwebui_forward_jwt_secret="identity-secret-with-at-least-32-bytes", # noqa: S106 openwebui_forward_jwt_secret="identity-secret-with-at-least-32-bytes", # noqa: S106
internal_provider_key="provider-secret-with-at-least-32-bytes", internal_provider_key="provider-secret-with-at-least-32-bytes",
internal_gateway_key="gateway-secret-with-at-least-32-bytes", internal_gateway_key="gateway-secret-with-at-least-32-bytes",
+56
View File
@@ -962,3 +962,59 @@ def test_tool_events_render_one_friendly_completed_card() -> None:
assert 'name="执行命令"' in rendered assert 'name="执行命令"' in rendered
assert "2 passed" in rendered assert "2 passed" in rendered
assert 'done="false"' not in rendered assert 'done="false"' not in rendered
async def test_extreme_tier_preserves_reasoning_state_across_tool_turns(settings) -> None:
provider = ScriptedProvider(
[
{
"choices": [
{
"message": {
"role": "assistant",
"content": None,
"reasoning_content": "provider-private-state",
"tool_calls": [tool_call("1", "read_file", {"path": "notes.txt"})],
},
"finish_reason": "tool_calls",
}
]
},
{
"choices": [
{
"message": {"role": "assistant", "content": "检查完成"},
"finish_reason": "stop",
}
]
},
]
)
registry = RecordingRegistry()
store = RuntimeStore(settings.database_url)
await store.initialize()
async def callback(event_type, payload):
return None
try:
answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run(
spec=get_model_spec("work-extreme"),
messages=[{"role": "user", "content": "读取 notes.txt 并检查内容"}],
identity=UserIdentity("u1", "", "", "user"),
raw_user_jwt="jwt",
chat_id="deepseek-reasoning",
callback=callback,
)
finally:
await store.close()
assert answer == "检查完成"
assert provider.requests[0]["provider"] == "deepseek"
assert provider.requests[0]["reasoning_effort"] == "max"
assistant_turn = next(
message
for message in provider.requests[1]["messages"]
if message.get("role") == "assistant" and message.get("tool_calls")
)
assert assistant_turn["reasoning_content"] == "provider-private-state"
+1 -1
View File
@@ -3,7 +3,7 @@ from agent_platform.bootstrap import _model_payload
def test_bootstrap_models_override_provider_ids_and_are_public() -> None: def test_bootstrap_models_override_provider_ids_and_are_public() -> None:
models = _model_payload() models = _model_payload()
assert len(models) == 6 assert len(models) == 8
assert all(model["base_model_id"] is None for model in models) assert all(model["base_model_id"] is None for model in models)
assert all( assert all(
model["access_grants"] == [{"principal_type": "user", "principal_id": "*", "permission": "read"}] model["access_grants"] == [{"principal_type": "user", "principal_id": "*", "permission": "read"}]
+5
View File
@@ -6,3 +6,8 @@ import pytest
def test_internal_secrets_must_be_at_least_32_bytes(settings) -> None: def test_internal_secrets_must_be_at_least_32_bytes(settings) -> None:
with pytest.raises(RuntimeError, match="at least 32 bytes"): with pytest.raises(RuntimeError, match="at least 32 bytes"):
replace(settings, internal_gateway_key="too-short").validate_gateway() replace(settings, internal_gateway_key="too-short").validate_gateway()
def test_runtime_requires_deepseek_key_for_extreme_tier(settings) -> None:
with pytest.raises(RuntimeError, match="DEEPSEEK_API_KEY"):
replace(settings, deepseek_api_key="").validate_runtime()
+7
View File
@@ -6,11 +6,18 @@ def test_fixed_public_model_matrix() -> None:
"chat-light", "chat-light",
"chat-medium", "chat-medium",
"chat-high", "chat-high",
"chat-extreme",
"work-light", "work-light",
"work-medium", "work-medium",
"work-high", "work-high",
"work-extreme",
} }
assert get_model_spec("chat-light").provider_model == "ChatGPT-5.6:Luna" 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-medium").provider_model == "ChatGPT-5.6:Terra"
assert get_model_spec("work-high").provider_model == "ChatGPT-5.6:Sol" 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) assert {item["id"] for item in openai_model_list()["data"]} == set(MODEL_SPECS)
+44 -1
View File
@@ -21,10 +21,15 @@ def disable_retry_sleep(monkeypatch: pytest.MonkeyPatch) -> None:
async def _complete_with_handler( async def _complete_with_handler(
settings: Settings, settings: Settings,
handler: Callable[[httpx.Request], httpx.Response | Awaitable[httpx.Response]], handler: Callable[[httpx.Request], httpx.Response | Awaitable[httpx.Response]],
**kwargs,
) -> dict: ) -> dict:
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
provider = ModelProvider(settings, client) provider = ModelProvider(settings, client)
return await provider.complete(model="test-model", messages=[{"role": "user", "content": "hello"}]) return await provider.complete(
model="test-model",
messages=[{"role": "user", "content": "hello"}],
**kwargs,
)
async def test_complete_retries_transient_gateway_response(settings: Settings) -> None: async def test_complete_retries_transient_gateway_response(settings: Settings) -> None:
@@ -158,3 +163,41 @@ async def test_complete_aggregates_streamed_tool_call(settings: Settings) -> Non
} }
] ]
assert result["usage"]["total_tokens"] == 7 assert result["usage"]["total_tokens"] == 7
async def test_deepseek_route_enables_thinking_and_uses_separate_credentials(settings: Settings) -> None:
body = (
'data: {"choices":[{"index":0,"delta":{"role":"assistant",'
'"reasoning_content":"private state","tool_calls":[{"index":0,"id":"call-1",'
'"type":"function","function":{"name":"ping","arguments":"{}"}}]},'
'"finish_reason":"tool_calls"}]}\n\n'
"data: [DONE]\n\n"
)
def handler(request: httpx.Request) -> httpx.Response:
assert str(request.url) == "https://api.deepseek.example.test/v1/chat/completions"
assert request.headers["Authorization"] == "Bearer deepseek-secret"
payload = json.loads(request.content)
assert payload["model"] == "test-model"
assert payload["thinking"] == {"type": "enabled"}
assert payload["reasoning_effort"] == "max"
assert payload["max_tokens"] == 16_384
return httpx.Response(
200,
request=request,
headers={"Content-Type": "text/event-stream"},
content=body,
)
result = await _complete_with_handler(
settings,
handler,
provider="deepseek",
thinking_enabled=True,
reasoning_effort="max",
max_output_tokens=16_384,
)
message = result["choices"][0]["message"]
assert message["reasoning_content"] == "private state"
assert message["tool_calls"][0]["function"]["name"] == "ping"
+28 -2
View File
@@ -13,6 +13,7 @@ class FakeModelProvider:
def __init__(self) -> None: def __init__(self) -> None:
self.completed = [] self.completed = []
self.forwarded = [] self.forwarded = []
self.forward_options = []
async def complete(self, **kwargs): async def complete(self, **kwargs):
self.completed.append(kwargs) self.completed.append(kwargs)
@@ -21,8 +22,9 @@ class FakeModelProvider:
"usage": {"total_tokens": 10}, "usage": {"total_tokens": 10},
} }
async def forward(self, payload): async def forward(self, payload, **kwargs):
self.forwarded.append(payload) self.forwarded.append(payload)
self.forward_options.append(kwargs)
request = httpx.Request("POST", "https://model.example.test/v1/chat/completions") request = httpx.Request("POST", "https://model.example.test/v1/chat/completions")
if payload.get("stream"): if payload.get("stream"):
return httpx.Response( return httpx.Response(
@@ -83,7 +85,7 @@ def test_model_list_requires_internal_key(settings) -> None:
headers={"Authorization": f"Bearer {settings.internal_provider_key}"}, headers={"Authorization": f"Bearer {settings.internal_provider_key}"},
) )
assert response.status_code == 200 assert response.status_code == 200
assert len(response.json()["data"]) == 6 assert len(response.json()["data"]) == 8
def test_chat_maps_provider_model_and_work_upgrade_is_one_way(settings, identity_jwt) -> None: def test_chat_maps_provider_model_and_work_upgrade_is_one_way(settings, identity_jwt) -> None:
@@ -99,6 +101,20 @@ def test_chat_maps_provider_model_and_work_upgrade_is_one_way(settings, identity
assert provider.forwarded[0]["model"] == "ChatGPT-5.6:Luna" assert provider.forwarded[0]["model"] == "ChatGPT-5.6:Luna"
assert chat.json()["model"] == "chat-light" 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,
}
work = client.post( work = client.post(
"/v1/chat/completions", "/v1/chat/completions",
headers=headers(settings, identity_jwt), headers=headers(settings, identity_jwt),
@@ -107,6 +123,16 @@ def test_chat_maps_provider_model_and_work_upgrade_is_one_way(settings, identity
assert work.status_code == 200 assert work.status_code == 200
assert provider.completed[-1]["model"] == "ChatGPT-5.6:Sol" assert provider.completed[-1]["model"] == "ChatGPT-5.6:Sol"
extreme_work = client.post(
"/v1/chat/completions",
headers=headers(settings, identity_jwt, "extreme-work"),
json={"model": "work-extreme", "messages": [{"role": "user", "content": "do it carefully"}]},
)
assert extreme_work.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( downgrade = client.post(
"/v1/chat/completions", "/v1/chat/completions",
headers=headers(settings, identity_jwt), headers=headers(settings, identity_jwt),