create-windy 0.6.10 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -0
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +19 -0
- package/template/apps/agent-server/tests/test_report_routes.py +86 -0
- package/template/apps/agent-server/tests/test_report_store.py +62 -1
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +10 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +1 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +3 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +43 -31
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +15 -4
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.vue +48 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.webtest.ts +32 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +2 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.webtest.ts +70 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +29 -2
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.webtest.ts +56 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +7 -10
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +115 -2
- package/template/docs/platform/agent-runtime-observability.md +6 -0
- package/template/docs/platform/agent-supervisor-output-contract.md +17 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +19 -0
package/dist/cli.js
CHANGED
|
@@ -8290,6 +8290,11 @@ var recipes = [
|
|
|
8290
8290
|
id: "starter-0.6.9-to-0.6.10",
|
|
8291
8291
|
from: "0.6.9",
|
|
8292
8292
|
to: "0.6.10"
|
|
8293
|
+
},
|
|
8294
|
+
{
|
|
8295
|
+
id: "starter-0.6.10-to-0.6.11",
|
|
8296
|
+
from: "0.6.10",
|
|
8297
|
+
to: "0.6.11"
|
|
8293
8298
|
}
|
|
8294
8299
|
];
|
|
8295
8300
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
package/package.json
CHANGED
|
@@ -237,12 +237,29 @@ def run_value(run: RunReport, attempts: list[AttemptReport]) -> dict[str, object
|
|
|
237
237
|
"finishedAt": iso(run.finished_at),
|
|
238
238
|
"durationMs": run.duration_ms,
|
|
239
239
|
"attemptCount": len(attempts),
|
|
240
|
+
"modelDeployments": model_deployments(attempts),
|
|
240
241
|
"models": models,
|
|
241
242
|
"cacheStatus": cache,
|
|
242
243
|
"usage": usage_value(attempts),
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
|
|
247
|
+
def model_deployments(attempts: Iterable[AttemptReport]) -> list[dict[str, object]]:
|
|
248
|
+
"""按首次 Attempt 顺序保留真实的模型/deployment 组合及出现次数。"""
|
|
249
|
+
groups: dict[tuple[str, str], int] = {}
|
|
250
|
+
for item in attempts:
|
|
251
|
+
key = (item.deployment_key, item.reported_model or item.requested_model)
|
|
252
|
+
groups[key] = groups.get(key, 0) + 1
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
"deploymentKey": deployment_key,
|
|
256
|
+
"model": model,
|
|
257
|
+
"attemptCount": attempt_count,
|
|
258
|
+
}
|
|
259
|
+
for (deployment_key, model), attempt_count in groups.items()
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
|
|
246
263
|
def usage_value(attempts: list[AttemptReport]) -> dict[str, int | None]:
|
|
247
264
|
def total(field: str) -> int | None:
|
|
248
265
|
values = [
|
|
@@ -267,6 +284,8 @@ def attempt_value(item: AttemptReport) -> dict[str, object]:
|
|
|
267
284
|
"sequence": item.sequence,
|
|
268
285
|
"toolRound": item.tool_round,
|
|
269
286
|
"deploymentKey": item.deployment_key,
|
|
287
|
+
"requestedModel": item.requested_model,
|
|
288
|
+
"reportedModel": item.reported_model,
|
|
270
289
|
"model": item.reported_model or item.requested_model,
|
|
271
290
|
"outcome": item.outcome,
|
|
272
291
|
"cacheStatus": item.usage.cache_status,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
from southwind_agent_server.contracts import RunReportingContext, StartRunRequest
|
|
8
|
+
from southwind_agent_server.report_routes import create_report_router
|
|
9
|
+
from southwind_agent_server.report_store import InMemoryAgentRuntimeReportStore
|
|
10
|
+
from southwind_agent_server.report_types import UsageFacts
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def test_internal_run_routes_project_model_deployments_and_facts(
|
|
14
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
15
|
+
) -> None:
|
|
16
|
+
monkeypatch.setattr("southwind_agent_server.report_routes.time.time", lambda: 400.0)
|
|
17
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
18
|
+
await store.ensure_run(
|
|
19
|
+
"run_1",
|
|
20
|
+
StartRunRequest(
|
|
21
|
+
operation_key="business.answer",
|
|
22
|
+
input=None,
|
|
23
|
+
reporting=RunReportingContext(
|
|
24
|
+
actor_id="actor_1", actor_type="user", department_id=None
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
100.0,
|
|
28
|
+
)
|
|
29
|
+
attempt = await store.start_attempt(
|
|
30
|
+
"run_1", 1, "ai.primary", "requested-model", 101.0
|
|
31
|
+
)
|
|
32
|
+
await store.finish_attempt(
|
|
33
|
+
attempt.attempt_id,
|
|
34
|
+
"completed",
|
|
35
|
+
UsageFacts(total_tokens=12),
|
|
36
|
+
102.0,
|
|
37
|
+
"provider-model-v2",
|
|
38
|
+
)
|
|
39
|
+
application = FastAPI()
|
|
40
|
+
application.include_router(create_report_router(store, "internal-token"))
|
|
41
|
+
|
|
42
|
+
async with httpx.AsyncClient(
|
|
43
|
+
transport=httpx.ASGITransport(app=application), base_url="http://test"
|
|
44
|
+
) as client:
|
|
45
|
+
headers = {"Authorization": "Bearer internal-token"}
|
|
46
|
+
listed = await client.get(
|
|
47
|
+
"/api/internal/ai-runtime-report/runs?range=retained&scopeMode=all",
|
|
48
|
+
headers=headers,
|
|
49
|
+
)
|
|
50
|
+
detail = await client.get(
|
|
51
|
+
"/api/internal/ai-runtime-report/runs/run_1", headers=headers
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert listed.status_code == 200
|
|
55
|
+
assert listed.json()["items"][0]["modelDeployments"] == [
|
|
56
|
+
{
|
|
57
|
+
"deploymentKey": "ai.primary",
|
|
58
|
+
"model": "provider-model-v2",
|
|
59
|
+
"attemptCount": 1,
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
assert detail.status_code == 200
|
|
63
|
+
assert detail.json()["attempts"] == [
|
|
64
|
+
{
|
|
65
|
+
"attemptId": attempt.attempt_id,
|
|
66
|
+
"sequence": 1,
|
|
67
|
+
"toolRound": 1,
|
|
68
|
+
"deploymentKey": "ai.primary",
|
|
69
|
+
"requestedModel": "requested-model",
|
|
70
|
+
"reportedModel": "provider-model-v2",
|
|
71
|
+
"model": "provider-model-v2",
|
|
72
|
+
"outcome": "completed",
|
|
73
|
+
"cacheStatus": "unknown",
|
|
74
|
+
"usageSource": "unknown",
|
|
75
|
+
"inputTokens": None,
|
|
76
|
+
"outputTokens": None,
|
|
77
|
+
"cachedInputTokens": None,
|
|
78
|
+
"reasoningTokens": None,
|
|
79
|
+
"totalTokens": 12,
|
|
80
|
+
"firstTokenLatencyMs": None,
|
|
81
|
+
"durationMs": 1000,
|
|
82
|
+
"safeErrorCode": None,
|
|
83
|
+
"startedAt": "1970-01-01T00:01:41Z",
|
|
84
|
+
"finishedAt": "1970-01-01T00:01:42Z",
|
|
85
|
+
}
|
|
86
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Any, Literal, cast
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
@@ -284,3 +284,64 @@ async def test_completely_unknown_aggregate_tokens_stay_null() -> None:
|
|
|
284
284
|
}
|
|
285
285
|
assert summary["byModel"][0]["totalTokens"] is None # type: ignore[index]
|
|
286
286
|
assert summary["activity"][0]["totalTokens"] is None # type: ignore[index]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def test_run_projection_keeps_model_deployment_combinations_and_facts() -> None:
|
|
290
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
291
|
+
await store.ensure_run("run_empty", request("actor_1"), 90.0)
|
|
292
|
+
await store.ensure_run("run_1", request("actor_1"), 100.0)
|
|
293
|
+
|
|
294
|
+
first = await store.start_attempt(
|
|
295
|
+
"run_1", 1, "ai.primary", "requested-primary", 101.0
|
|
296
|
+
)
|
|
297
|
+
await store.start_attempt("run_1", 2, "ai.fallback", "requested-fallback", 102.0)
|
|
298
|
+
third = await store.start_attempt(
|
|
299
|
+
"run_1", 3, "ai.primary", "requested-primary", 103.0
|
|
300
|
+
)
|
|
301
|
+
await store.finish_attempt(
|
|
302
|
+
first.attempt_id,
|
|
303
|
+
"completed",
|
|
304
|
+
UsageFacts(),
|
|
305
|
+
104.0,
|
|
306
|
+
"provider-primary-v2",
|
|
307
|
+
)
|
|
308
|
+
await store.finish_attempt(third.attempt_id, "failed", UsageFacts(), 105.0)
|
|
309
|
+
|
|
310
|
+
page = await store.list_runs(ReportFilter(0, 500), 1, 20)
|
|
311
|
+
items = cast(list[dict[str, Any]], page["items"])
|
|
312
|
+
item = items[0]
|
|
313
|
+
assert item["modelDeployments"] == [
|
|
314
|
+
{
|
|
315
|
+
"deploymentKey": "ai.primary",
|
|
316
|
+
"model": "provider-primary-v2",
|
|
317
|
+
"attemptCount": 1,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"deploymentKey": "ai.fallback",
|
|
321
|
+
"model": "requested-fallback",
|
|
322
|
+
"attemptCount": 1,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"deploymentKey": "ai.primary",
|
|
326
|
+
"model": "requested-primary",
|
|
327
|
+
"attemptCount": 1,
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
assert items[1]["modelDeployments"] == []
|
|
331
|
+
|
|
332
|
+
detail = await store.get_run("run_1")
|
|
333
|
+
assert detail is not None
|
|
334
|
+
attempts = cast(list[dict[str, Any]], detail["attempts"])
|
|
335
|
+
assert [
|
|
336
|
+
(
|
|
337
|
+
attempt["sequence"],
|
|
338
|
+
attempt["deploymentKey"],
|
|
339
|
+
attempt["requestedModel"],
|
|
340
|
+
attempt["reportedModel"],
|
|
341
|
+
)
|
|
342
|
+
for attempt in attempts
|
|
343
|
+
] == [
|
|
344
|
+
(1, "ai.primary", "requested-primary", "provider-primary-v2"),
|
|
345
|
+
(2, "ai.fallback", "requested-fallback", None),
|
|
346
|
+
(3, "ai.primary", "requested-primary", None),
|
|
347
|
+
]
|
|
@@ -111,6 +111,14 @@ function parseReportRun(value: unknown): InternalAiRuntimeRunListItem {
|
|
|
111
111
|
finishedAt: nullableText(item.finishedAt),
|
|
112
112
|
durationMs: nullableInteger(item.durationMs),
|
|
113
113
|
attemptCount: integer(item.attemptCount),
|
|
114
|
+
modelDeployments: array(item.modelDeployments).map((modelDeployment) => {
|
|
115
|
+
const combination = record(modelDeployment);
|
|
116
|
+
return {
|
|
117
|
+
deploymentKey: text(combination.deploymentKey),
|
|
118
|
+
model: text(combination.model),
|
|
119
|
+
attemptCount: integer(combination.attemptCount),
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
114
122
|
models: array(item.models).map(text),
|
|
115
123
|
cacheStatus: oneOf(item.cacheStatus, ["hit", "miss", "unknown"]),
|
|
116
124
|
usage: parseUsage(item.usage),
|
|
@@ -124,6 +132,8 @@ function parseAttempt(value: unknown): AiRuntimeAttemptDetail {
|
|
|
124
132
|
sequence: integer(item.sequence),
|
|
125
133
|
toolRound: integer(item.toolRound),
|
|
126
134
|
deploymentKey: text(item.deploymentKey),
|
|
135
|
+
requestedModel: text(item.requestedModel),
|
|
136
|
+
reportedModel: nullableText(item.reportedModel),
|
|
127
137
|
model: text(item.model),
|
|
128
138
|
outcome: oneOf(item.outcome, [
|
|
129
139
|
"running",
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@/components/ui/card";
|
|
12
12
|
import AiRuntimeActivityChart from "./components/ai-runtime/AiRuntimeActivityChart.vue";
|
|
13
13
|
import AiRuntimeFilters from "./components/ai-runtime/AiRuntimeFilters.vue";
|
|
14
|
+
import AiRuntimeModelUsageList from "./components/ai-runtime/AiRuntimeModelUsageList.vue";
|
|
14
15
|
import AiRuntimeRunDetailDialog from "./components/ai-runtime/AiRuntimeRunDetailDialog.vue";
|
|
15
16
|
import AiRuntimeRunTable from "./components/ai-runtime/AiRuntimeRunTable.vue";
|
|
16
17
|
import AiRuntimeSummaryCards from "./components/ai-runtime/AiRuntimeSummaryCards.vue";
|
|
@@ -80,7 +81,8 @@ const retentionDescription = computed(() => {
|
|
|
80
81
|
模型与 deployment 仅用于授权报表,不进入 Prometheus 高基数标签。
|
|
81
82
|
</CardDescription>
|
|
82
83
|
</CardHeader>
|
|
83
|
-
<CardContent>
|
|
84
|
+
<CardContent class="space-y-5">
|
|
85
|
+
<AiRuntimeModelUsageList :models="report.summary.value.byModel" />
|
|
84
86
|
<AiRuntimeTokenByModelChart
|
|
85
87
|
:models="report.summary.value.byModel"
|
|
86
88
|
/>
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { shallowRef } from "vue";
|
|
2
|
+
import { computed, shallowRef } from "vue";
|
|
3
3
|
import { SearchIcon } from "@lucide/vue";
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "@/components/ui/select";
|
|
5
13
|
import type { AiRuntimeActor, AiRuntimeRange } from "@/services/ai-runtime-api";
|
|
6
14
|
|
|
7
|
-
defineProps<{
|
|
15
|
+
const props = defineProps<{
|
|
8
16
|
range: AiRuntimeRange;
|
|
9
17
|
actorHandle?: string;
|
|
10
18
|
actors: readonly AiRuntimeActor[];
|
|
@@ -17,6 +25,12 @@ const emit = defineEmits<{
|
|
|
17
25
|
"search:actors": [value: string];
|
|
18
26
|
}>();
|
|
19
27
|
const actorSearch = shallowRef("");
|
|
28
|
+
const allActorsValue = "__all_actors__";
|
|
29
|
+
const selectedActor = computed({
|
|
30
|
+
get: () => props.actorHandle ?? allActorsValue,
|
|
31
|
+
set: (value: string) =>
|
|
32
|
+
emit("update:actorHandle", value === allActorsValue ? undefined : value),
|
|
33
|
+
});
|
|
20
34
|
|
|
21
35
|
const ranges: readonly { value: AiRuntimeRange; label: string }[] = [
|
|
22
36
|
{ value: "day", label: "近 24 小时" },
|
|
@@ -24,11 +38,6 @@ const ranges: readonly { value: AiRuntimeRange; label: string }[] = [
|
|
|
24
38
|
{ value: "retained", label: "保留期" },
|
|
25
39
|
];
|
|
26
40
|
|
|
27
|
-
function selectActor(event: Event) {
|
|
28
|
-
const value = (event.target as HTMLSelectElement).value;
|
|
29
|
-
emit("update:actorHandle", value || undefined);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
41
|
function searchActors() {
|
|
33
42
|
emit("search:actors", actorSearch.value.trim());
|
|
34
43
|
}
|
|
@@ -38,11 +47,14 @@ function searchActors() {
|
|
|
38
47
|
<div
|
|
39
48
|
class="flex flex-col gap-3 rounded-lg border bg-card p-3 md:flex-row md:items-center md:justify-between"
|
|
40
49
|
>
|
|
41
|
-
<div
|
|
50
|
+
<div
|
|
51
|
+
class="inline-flex min-h-9 w-fit items-center gap-1 rounded-md bg-muted p-0.5"
|
|
52
|
+
>
|
|
42
53
|
<Button
|
|
43
54
|
v-for="item in ranges"
|
|
44
55
|
:key="item.value"
|
|
45
56
|
size="sm"
|
|
57
|
+
class="h-8 px-3"
|
|
46
58
|
:variant="range === item.value ? 'secondary' : 'ghost'"
|
|
47
59
|
:disabled="disabled"
|
|
48
60
|
@click="emit('update:range', item.value)"
|
|
@@ -51,19 +63,19 @@ function searchActors() {
|
|
|
51
63
|
</Button>
|
|
52
64
|
</div>
|
|
53
65
|
|
|
54
|
-
<div class="flex flex-col gap-2 sm:flex-row sm:items-
|
|
55
|
-
<label class="
|
|
66
|
+
<div class="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-end">
|
|
67
|
+
<label class="grid gap-1 text-sm text-muted-foreground">
|
|
56
68
|
查找
|
|
57
|
-
<span class="flex">
|
|
58
|
-
<
|
|
69
|
+
<span class="flex min-w-0">
|
|
70
|
+
<Input
|
|
59
71
|
v-model="actorSearch"
|
|
60
|
-
class="h-9 w-
|
|
72
|
+
class="h-9 w-full rounded-r-none border-r-0 focus-visible:z-10 sm:w-44"
|
|
61
73
|
placeholder="姓名或账号"
|
|
62
74
|
:disabled="disabled"
|
|
63
75
|
@keyup.enter="searchActors"
|
|
64
76
|
/>
|
|
65
77
|
<Button
|
|
66
|
-
class="rounded-l-none"
|
|
78
|
+
class="h-9 w-9 shrink-0 rounded-l-none"
|
|
67
79
|
size="icon"
|
|
68
80
|
variant="outline"
|
|
69
81
|
:disabled="disabled"
|
|
@@ -74,24 +86,24 @@ function searchActors() {
|
|
|
74
86
|
</Button>
|
|
75
87
|
</span>
|
|
76
88
|
</label>
|
|
77
|
-
<label class="
|
|
89
|
+
<label class="grid gap-1 text-sm text-muted-foreground">
|
|
78
90
|
操作者
|
|
79
|
-
<
|
|
80
|
-
class="h-9
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</
|
|
94
|
-
</
|
|
91
|
+
<Select v-model="selectedActor" :disabled="disabled">
|
|
92
|
+
<SelectTrigger class="h-9 w-full sm:w-48" aria-label="筛选操作者">
|
|
93
|
+
<SelectValue placeholder="全部操作者" />
|
|
94
|
+
</SelectTrigger>
|
|
95
|
+
<SelectContent>
|
|
96
|
+
<SelectItem :value="allActorsValue">全部操作者</SelectItem>
|
|
97
|
+
<SelectItem
|
|
98
|
+
v-for="actor in actors"
|
|
99
|
+
:key="actor.handle"
|
|
100
|
+
:value="actor.handle"
|
|
101
|
+
>
|
|
102
|
+
{{ actor.displayName
|
|
103
|
+
}}{{ actor.username ? `(${actor.username})` : "" }}
|
|
104
|
+
</SelectItem>
|
|
105
|
+
</SelectContent>
|
|
106
|
+
</Select>
|
|
95
107
|
</label>
|
|
96
108
|
</div>
|
|
97
109
|
</div>
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { mount } from "@vue/test-utils";
|
|
2
|
-
import { describe, expect, test } from "vitest";
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
3
|
+
import { Select } from "@/components/ui/select";
|
|
3
4
|
import AiRuntimeFilters from "./AiRuntimeFilters.vue";
|
|
4
5
|
|
|
6
|
+
afterEach(() => document.body.replaceChildren());
|
|
7
|
+
|
|
5
8
|
describe("AI Runtime 操作者筛选", () => {
|
|
6
|
-
test("
|
|
9
|
+
test("按姓名搜索后通过设计系统 Select 选择服务端返回的操作者", async () => {
|
|
7
10
|
const wrapper = mount(AiRuntimeFilters, {
|
|
8
11
|
props: {
|
|
9
12
|
range: "day",
|
|
@@ -19,9 +22,17 @@ describe("AI Runtime 操作者筛选", () => {
|
|
|
19
22
|
|
|
20
23
|
await wrapper.get('input[placeholder="姓名或账号"]').setValue("张三");
|
|
21
24
|
await wrapper.get('button[aria-label="搜索操作者"]').trigger("click");
|
|
22
|
-
|
|
25
|
+
wrapper.findComponent(Select).vm.$emit("update:modelValue", "actor_opaque");
|
|
26
|
+
await flushPromises();
|
|
23
27
|
|
|
24
28
|
expect(wrapper.emitted("search:actors")).toEqual([["张三"]]);
|
|
25
29
|
expect(wrapper.emitted("update:actorHandle")).toEqual([["actor_opaque"]]);
|
|
30
|
+
expect(wrapper.find("select").exists()).toBe(false);
|
|
31
|
+
expect(
|
|
32
|
+
wrapper.get('[data-slot="select-trigger"]').attributes("aria-label"),
|
|
33
|
+
).toBe("筛选操作者");
|
|
34
|
+
expect(wrapper.get('[data-slot="select-trigger"]').attributes("role")).toBe(
|
|
35
|
+
"combobox",
|
|
36
|
+
);
|
|
26
37
|
});
|
|
27
38
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import type { AiRuntimeModelUsage } from "@/services/ai-runtime-api";
|
|
4
|
+
import { formatTokenCount } from "../../ai-runtime-presenter";
|
|
5
|
+
|
|
6
|
+
defineProps<{ models: readonly AiRuntimeModelUsage[] }>();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<section aria-labelledby="ai-runtime-model-usage-title" class="space-y-2">
|
|
11
|
+
<div>
|
|
12
|
+
<h3 id="ai-runtime-model-usage-title" class="text-sm font-medium">
|
|
13
|
+
当前范围的模型与 deployment
|
|
14
|
+
</h3>
|
|
15
|
+
<p class="text-xs text-muted-foreground">
|
|
16
|
+
模型名优先采用 Provider 回报;未回报时显示请求模型。Token 未上报不会按 0
|
|
17
|
+
处理。
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<ul
|
|
21
|
+
v-if="models.length"
|
|
22
|
+
class="flex flex-wrap gap-2"
|
|
23
|
+
aria-label="当前筛选范围内的模型 Token 用量"
|
|
24
|
+
>
|
|
25
|
+
<li
|
|
26
|
+
v-for="item in models"
|
|
27
|
+
:key="`${item.deploymentKey}:${item.model}`"
|
|
28
|
+
class="flex min-w-0 items-start gap-1.5 rounded-md border bg-muted/30 px-2 py-1 text-xs"
|
|
29
|
+
>
|
|
30
|
+
<Badge
|
|
31
|
+
variant="secondary"
|
|
32
|
+
class="h-auto whitespace-normal break-all py-0.5 font-medium"
|
|
33
|
+
>
|
|
34
|
+
{{ item.model }}
|
|
35
|
+
</Badge>
|
|
36
|
+
<span class="min-w-0 break-all text-muted-foreground">
|
|
37
|
+
{{ item.deploymentKey }}
|
|
38
|
+
</span>
|
|
39
|
+
<span class="whitespace-nowrap tabular-nums text-foreground">
|
|
40
|
+
{{ formatTokenCount(item.totalTokens) }} Token
|
|
41
|
+
</span>
|
|
42
|
+
</li>
|
|
43
|
+
</ul>
|
|
44
|
+
<p v-else class="text-sm text-muted-foreground">
|
|
45
|
+
当前范围内没有已上报的模型事实。
|
|
46
|
+
</p>
|
|
47
|
+
</section>
|
|
48
|
+
</template>
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.webtest.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import AiRuntimeModelUsageList from "./AiRuntimeModelUsageList.vue";
|
|
4
|
+
|
|
5
|
+
describe("AI Runtime 模型用量列表", () => {
|
|
6
|
+
test("模型、deployment 和 Token 在图表外始终可读", () => {
|
|
7
|
+
const wrapper = mount(AiRuntimeModelUsageList, {
|
|
8
|
+
props: {
|
|
9
|
+
models: [
|
|
10
|
+
{
|
|
11
|
+
deploymentKey: "ai.primary",
|
|
12
|
+
model: "provider-model-v2",
|
|
13
|
+
attempts: 2,
|
|
14
|
+
inputTokens: 10,
|
|
15
|
+
outputTokens: 5,
|
|
16
|
+
cachedInputTokens: null,
|
|
17
|
+
reasoningTokens: null,
|
|
18
|
+
totalTokens: 15,
|
|
19
|
+
unknownUsageCount: 0,
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(wrapper.text()).toContain("provider-model-v2");
|
|
26
|
+
expect(wrapper.text()).toContain("ai.primary");
|
|
27
|
+
expect(wrapper.text()).toContain("15 Token");
|
|
28
|
+
expect(
|
|
29
|
+
wrapper.find("[aria-label='当前筛选范围内的模型 Token 用量']").exists(),
|
|
30
|
+
).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue
CHANGED
|
@@ -127,7 +127,8 @@ const open = defineModel<boolean>("open", { required: true });
|
|
|
127
127
|
{{ attempt.reportedModel || attempt.requestedModel }}
|
|
128
128
|
</div>
|
|
129
129
|
<div class="text-xs text-muted-foreground">
|
|
130
|
-
{{ attempt.deploymentKey }}
|
|
130
|
+
{{ attempt.deploymentKey }} ·
|
|
131
|
+
{{ attempt.reportedModel ? "Provider 回报" : "请求模型" }}
|
|
131
132
|
</div>
|
|
132
133
|
</TableCell>
|
|
133
134
|
<TableCell>{{ runStatusLabel(attempt.outcome) }}</TableCell>
|
package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.webtest.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
3
|
+
import AiRuntimeRunDetailDialog from "./AiRuntimeRunDetailDialog.vue";
|
|
4
|
+
|
|
5
|
+
afterEach(() => document.body.replaceChildren());
|
|
6
|
+
|
|
7
|
+
describe("AI Runtime Run 详情", () => {
|
|
8
|
+
test("按 Attempt sequence 保留 deployment、请求模型和 Provider 回报模型", async () => {
|
|
9
|
+
const wrapper = mount(AiRuntimeRunDetailDialog, {
|
|
10
|
+
attachTo: document.body,
|
|
11
|
+
props: {
|
|
12
|
+
open: true,
|
|
13
|
+
detail: {
|
|
14
|
+
runId: "run_1",
|
|
15
|
+
operationKey: "business.answer",
|
|
16
|
+
actor: null,
|
|
17
|
+
status: "completed",
|
|
18
|
+
attempts: 2,
|
|
19
|
+
modelDeployments: [],
|
|
20
|
+
totalTokens: 12,
|
|
21
|
+
cacheStatus: "unknown",
|
|
22
|
+
releaseId: "release_1",
|
|
23
|
+
traceId: "trace_1",
|
|
24
|
+
attemptItems: [
|
|
25
|
+
{
|
|
26
|
+
sequence: 1,
|
|
27
|
+
deploymentKey: "ai.primary",
|
|
28
|
+
requestedModel: "requested-a",
|
|
29
|
+
reportedModel: "provider-a",
|
|
30
|
+
outcome: "completed",
|
|
31
|
+
inputTokens: null,
|
|
32
|
+
outputTokens: null,
|
|
33
|
+
cachedInputTokens: null,
|
|
34
|
+
reasoningTokens: null,
|
|
35
|
+
totalTokens: 10,
|
|
36
|
+
cacheStatus: "unknown",
|
|
37
|
+
startedAt: "2026-07-31T00:00:00.000Z",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
sequence: 2,
|
|
41
|
+
deploymentKey: "ai.fallback",
|
|
42
|
+
requestedModel: "requested-b",
|
|
43
|
+
reportedModel: null,
|
|
44
|
+
outcome: "failed",
|
|
45
|
+
inputTokens: null,
|
|
46
|
+
outputTokens: null,
|
|
47
|
+
cachedInputTokens: null,
|
|
48
|
+
reasoningTokens: null,
|
|
49
|
+
totalTokens: null,
|
|
50
|
+
cacheStatus: "unknown",
|
|
51
|
+
startedAt: "2026-07-31T00:00:01.000Z",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
await flushPromises();
|
|
58
|
+
|
|
59
|
+
const rows = [...document.querySelectorAll("tbody tr")];
|
|
60
|
+
expect(rows.map((row) => row.textContent)).toEqual(
|
|
61
|
+
expect.arrayContaining([
|
|
62
|
+
expect.stringContaining("provider-a"),
|
|
63
|
+
expect.stringContaining("requested-b"),
|
|
64
|
+
]),
|
|
65
|
+
);
|
|
66
|
+
expect(rows[0]?.textContent).toContain("ai.primary · Provider 回报");
|
|
67
|
+
expect(rows[1]?.textContent).toContain("ai.fallback · 请求模型");
|
|
68
|
+
wrapper.unmount();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TableRow,
|
|
12
12
|
} from "@/components/ui/table";
|
|
13
13
|
import type {
|
|
14
|
+
AiRuntimeModelDeployment,
|
|
14
15
|
AiRuntimePage,
|
|
15
16
|
AiRuntimeRunItem,
|
|
16
17
|
} from "@/services/ai-runtime-api";
|
|
@@ -40,11 +41,15 @@ function statusVariant(status: string) {
|
|
|
40
41
|
? "secondary"
|
|
41
42
|
: "outline";
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
function modelDeploymentLabel(item: AiRuntimeModelDeployment) {
|
|
46
|
+
return `${item.model} · ${item.deploymentKey}`;
|
|
47
|
+
}
|
|
43
48
|
</script>
|
|
44
49
|
|
|
45
50
|
<template>
|
|
46
51
|
<div class="space-y-3">
|
|
47
|
-
<div class="overflow-
|
|
52
|
+
<div class="overflow-x-auto rounded-md border">
|
|
48
53
|
<Table>
|
|
49
54
|
<TableHeader>
|
|
50
55
|
<TableRow>
|
|
@@ -105,7 +110,29 @@ function statusVariant(status: string) {
|
|
|
105
110
|
{{ runStatusLabel(run.status) }}
|
|
106
111
|
</Badge>
|
|
107
112
|
</TableCell>
|
|
108
|
-
<TableCell
|
|
113
|
+
<TableCell class="min-w-64">
|
|
114
|
+
<ul
|
|
115
|
+
v-if="run.modelDeployments.length"
|
|
116
|
+
class="flex flex-wrap gap-1"
|
|
117
|
+
:aria-label="`Run ${run.runId} 使用的模型与 deployment`"
|
|
118
|
+
>
|
|
119
|
+
<li
|
|
120
|
+
v-for="item in run.modelDeployments"
|
|
121
|
+
:key="`${item.deploymentKey}:${item.model}`"
|
|
122
|
+
>
|
|
123
|
+
<Badge
|
|
124
|
+
variant="outline"
|
|
125
|
+
class="h-auto max-w-60 whitespace-normal break-all py-0.5 text-left leading-4"
|
|
126
|
+
>
|
|
127
|
+
{{ modelDeploymentLabel(item) }}
|
|
128
|
+
<span v-if="item.attemptCount > 1" class="ml-1">
|
|
129
|
+
×{{ item.attemptCount }}
|
|
130
|
+
</span>
|
|
131
|
+
</Badge>
|
|
132
|
+
</li>
|
|
133
|
+
</ul>
|
|
134
|
+
<span v-else class="text-muted-foreground">未上报</span>
|
|
135
|
+
</TableCell>
|
|
109
136
|
<TableCell class="text-right tabular-nums">
|
|
110
137
|
{{ formatTokenCount(run.totalTokens) }}
|
|
111
138
|
</TableCell>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mount } from "@vue/test-utils";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import AiRuntimeRunTable from "./AiRuntimeRunTable.vue";
|
|
4
|
+
|
|
5
|
+
describe("AI Runtime Run 列表", () => {
|
|
6
|
+
test("完整展示多个模型与 deployment 组合,而不是折叠成首项", () => {
|
|
7
|
+
const wrapper = mount(AiRuntimeRunTable, {
|
|
8
|
+
props: {
|
|
9
|
+
runs: {
|
|
10
|
+
items: [
|
|
11
|
+
{
|
|
12
|
+
runId: "run_1",
|
|
13
|
+
operationKey: "business.answer",
|
|
14
|
+
actor: null,
|
|
15
|
+
status: "completed",
|
|
16
|
+
attempts: 3,
|
|
17
|
+
modelDeployments: [
|
|
18
|
+
{
|
|
19
|
+
deploymentKey: "ai.primary",
|
|
20
|
+
model: "provider-a",
|
|
21
|
+
attemptCount: 2,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
deploymentKey: "ai.fallback",
|
|
25
|
+
model: "requested-b",
|
|
26
|
+
attemptCount: 1,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
totalTokens: 12,
|
|
30
|
+
cacheStatus: "unknown",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
runId: "run_2",
|
|
34
|
+
operationKey: "business.answer",
|
|
35
|
+
actor: null,
|
|
36
|
+
status: "failed",
|
|
37
|
+
attempts: 0,
|
|
38
|
+
modelDeployments: [],
|
|
39
|
+
totalTokens: null,
|
|
40
|
+
cacheStatus: "unknown",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
total: 2,
|
|
44
|
+
page: 1,
|
|
45
|
+
pageSize: 20,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(wrapper.text()).toContain("provider-a · ai.primary");
|
|
51
|
+
expect(wrapper.text()).toContain("×2");
|
|
52
|
+
expect(wrapper.text()).toContain("requested-b · ai.fallback");
|
|
53
|
+
expect(wrapper.text()).toContain("未上报");
|
|
54
|
+
expect(wrapper.find(".overflow-x-auto").exists()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AiRuntimeActorSummary,
|
|
4
4
|
AiRuntimeAttemptDetail,
|
|
5
5
|
AiRuntimeReportSummary,
|
|
6
|
+
AiRuntimeRunModelDeployment,
|
|
6
7
|
AiRuntimeRunDetail as RuntimeRunDetail,
|
|
7
8
|
AiRuntimeRunListItem,
|
|
8
9
|
PageResult,
|
|
@@ -18,6 +19,7 @@ export interface AiRuntimeReportQuery {
|
|
|
18
19
|
export type AiRuntimeSummary = AiRuntimeReportSummary;
|
|
19
20
|
export type AiRuntimeModelUsage = AiRuntimeReportSummary["byModel"][number];
|
|
20
21
|
export type AiRuntimeActivity = AiRuntimeReportSummary["activity"][number];
|
|
22
|
+
export type AiRuntimeModelDeployment = AiRuntimeRunModelDeployment;
|
|
21
23
|
|
|
22
24
|
export interface AiRuntimeRunItem {
|
|
23
25
|
readonly runId: string;
|
|
@@ -28,7 +30,7 @@ export interface AiRuntimeRunItem {
|
|
|
28
30
|
readonly finishedAt?: string;
|
|
29
31
|
readonly durationMs?: number;
|
|
30
32
|
readonly attempts: number;
|
|
31
|
-
readonly
|
|
33
|
+
readonly modelDeployments: readonly AiRuntimeModelDeployment[];
|
|
32
34
|
readonly totalTokens: number | null;
|
|
33
35
|
readonly cacheStatus: "hit" | "miss" | "unknown";
|
|
34
36
|
readonly traceId?: string;
|
|
@@ -39,7 +41,7 @@ export interface AiRuntimeAttempt {
|
|
|
39
41
|
readonly sequence: number;
|
|
40
42
|
readonly deploymentKey: string;
|
|
41
43
|
readonly requestedModel: string;
|
|
42
|
-
readonly reportedModel
|
|
44
|
+
readonly reportedModel: string | null;
|
|
43
45
|
readonly outcome: string;
|
|
44
46
|
readonly inputTokens: number | null;
|
|
45
47
|
readonly outputTokens: number | null;
|
|
@@ -119,7 +121,7 @@ function runItem(value: AiRuntimeRunListItem): AiRuntimeRunItem {
|
|
|
119
121
|
...(value.finishedAt ? { finishedAt: value.finishedAt } : {}),
|
|
120
122
|
...(value.durationMs === null ? {} : { durationMs: value.durationMs }),
|
|
121
123
|
attempts: value.attemptCount,
|
|
122
|
-
|
|
124
|
+
modelDeployments: value.modelDeployments,
|
|
123
125
|
totalTokens: value.usage.totalTokens,
|
|
124
126
|
cacheStatus: value.cacheStatus,
|
|
125
127
|
};
|
|
@@ -139,7 +141,8 @@ function attemptItem(value: AiRuntimeAttemptDetail): AiRuntimeAttempt {
|
|
|
139
141
|
return {
|
|
140
142
|
sequence: value.sequence,
|
|
141
143
|
deploymentKey: value.deploymentKey,
|
|
142
|
-
requestedModel: value.
|
|
144
|
+
requestedModel: value.requestedModel,
|
|
145
|
+
reportedModel: value.reportedModel,
|
|
143
146
|
outcome: value.outcome,
|
|
144
147
|
inputTokens: value.inputTokens,
|
|
145
148
|
outputTokens: value.outputTokens,
|
|
@@ -156,9 +159,3 @@ function attemptItem(value: AiRuntimeAttemptDetail): AiRuntimeAttempt {
|
|
|
156
159
|
...(value.finishedAt ? { finishedAt: value.finishedAt } : {}),
|
|
157
160
|
};
|
|
158
161
|
}
|
|
159
|
-
|
|
160
|
-
function modelLabel(models: readonly string[]) {
|
|
161
|
-
if (models.length === 0) return undefined;
|
|
162
|
-
if (models.length === 1) return models[0];
|
|
163
|
-
return `${models[0]} 等 ${models.length} 个`;
|
|
164
|
-
}
|
|
@@ -25,6 +25,18 @@ describe("AI Runtime 报表 API", () => {
|
|
|
25
25
|
finishedAt: null,
|
|
26
26
|
durationMs: null,
|
|
27
27
|
attemptCount: 2,
|
|
28
|
+
modelDeployments: [
|
|
29
|
+
{
|
|
30
|
+
deploymentKey: "ai.primary",
|
|
31
|
+
model: "provider-a",
|
|
32
|
+
attemptCount: 1,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
deploymentKey: "ai.fallback",
|
|
36
|
+
model: "requested-b",
|
|
37
|
+
attemptCount: 1,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
28
40
|
models: ["gpt-a", "gpt-b"],
|
|
29
41
|
cacheStatus: "unknown",
|
|
30
42
|
usage: {
|
|
@@ -62,7 +74,18 @@ describe("AI Runtime 报表 API", () => {
|
|
|
62
74
|
);
|
|
63
75
|
expect(page.items[0]).toMatchObject({
|
|
64
76
|
attempts: 2,
|
|
65
|
-
|
|
77
|
+
modelDeployments: [
|
|
78
|
+
{
|
|
79
|
+
deploymentKey: "ai.primary",
|
|
80
|
+
model: "provider-a",
|
|
81
|
+
attemptCount: 1,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
deploymentKey: "ai.fallback",
|
|
85
|
+
model: "requested-b",
|
|
86
|
+
attemptCount: 1,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
66
89
|
totalTokens: null,
|
|
67
90
|
cacheStatus: "unknown",
|
|
68
91
|
});
|
|
@@ -79,6 +102,7 @@ describe("AI Runtime 报表 API", () => {
|
|
|
79
102
|
finishedAt: null,
|
|
80
103
|
durationMs: null,
|
|
81
104
|
attemptCount: 0,
|
|
105
|
+
modelDeployments: [],
|
|
82
106
|
models: [],
|
|
83
107
|
cacheStatus: "unknown",
|
|
84
108
|
usage: {
|
|
@@ -91,7 +115,30 @@ describe("AI Runtime 报表 API", () => {
|
|
|
91
115
|
releaseId: "release_1",
|
|
92
116
|
traceId: "trace_1",
|
|
93
117
|
safeErrorCode: null,
|
|
94
|
-
attempts: [
|
|
118
|
+
attempts: [
|
|
119
|
+
{
|
|
120
|
+
attemptId: "attempt_1",
|
|
121
|
+
sequence: 1,
|
|
122
|
+
toolRound: 1,
|
|
123
|
+
deploymentKey: "ai.primary",
|
|
124
|
+
requestedModel: "requested-model",
|
|
125
|
+
reportedModel: "provider-model",
|
|
126
|
+
model: "provider-model",
|
|
127
|
+
outcome: "completed",
|
|
128
|
+
cacheStatus: "unknown",
|
|
129
|
+
usageSource: "provider-reported",
|
|
130
|
+
inputTokens: null,
|
|
131
|
+
outputTokens: null,
|
|
132
|
+
cachedInputTokens: null,
|
|
133
|
+
reasoningTokens: null,
|
|
134
|
+
totalTokens: null,
|
|
135
|
+
firstTokenLatencyMs: null,
|
|
136
|
+
durationMs: null,
|
|
137
|
+
safeErrorCode: null,
|
|
138
|
+
startedAt: "2026-07-31T00:00:00.000Z",
|
|
139
|
+
finishedAt: null,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
95
142
|
})
|
|
96
143
|
.mockResolvedValueOnce({ items: [], total: 0, page: 1, pageSize: 50 });
|
|
97
144
|
|
|
@@ -108,4 +155,70 @@ describe("AI Runtime 报表 API", () => {
|
|
|
108
155
|
"/system/ai-runtime/actors?page=1&pageSize=50&q=%E5%BC%A0+%E4%B8%89",
|
|
109
156
|
);
|
|
110
157
|
});
|
|
158
|
+
|
|
159
|
+
test("详情保留 Provider 回报与请求模型的原始区别", async () => {
|
|
160
|
+
mocks.apiRequest.mockResolvedValueOnce({
|
|
161
|
+
runId: "run_1",
|
|
162
|
+
operationKey: "customer.answer",
|
|
163
|
+
actor: null,
|
|
164
|
+
status: "completed",
|
|
165
|
+
startedAt: null,
|
|
166
|
+
finishedAt: null,
|
|
167
|
+
durationMs: null,
|
|
168
|
+
attemptCount: 1,
|
|
169
|
+
modelDeployments: [
|
|
170
|
+
{
|
|
171
|
+
deploymentKey: "ai.primary",
|
|
172
|
+
model: "provider-model",
|
|
173
|
+
attemptCount: 1,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
models: ["provider-model"],
|
|
177
|
+
cacheStatus: "unknown",
|
|
178
|
+
usage: {
|
|
179
|
+
inputTokens: null,
|
|
180
|
+
outputTokens: null,
|
|
181
|
+
cachedInputTokens: null,
|
|
182
|
+
reasoningTokens: null,
|
|
183
|
+
totalTokens: null,
|
|
184
|
+
},
|
|
185
|
+
releaseId: "release_1",
|
|
186
|
+
traceId: "trace_1",
|
|
187
|
+
safeErrorCode: null,
|
|
188
|
+
attempts: [
|
|
189
|
+
{
|
|
190
|
+
attemptId: "attempt_1",
|
|
191
|
+
sequence: 1,
|
|
192
|
+
toolRound: 1,
|
|
193
|
+
deploymentKey: "ai.primary",
|
|
194
|
+
requestedModel: "requested-model",
|
|
195
|
+
reportedModel: "provider-model",
|
|
196
|
+
model: "provider-model",
|
|
197
|
+
outcome: "completed",
|
|
198
|
+
cacheStatus: "unknown",
|
|
199
|
+
usageSource: "provider-reported",
|
|
200
|
+
inputTokens: null,
|
|
201
|
+
outputTokens: null,
|
|
202
|
+
cachedInputTokens: null,
|
|
203
|
+
reasoningTokens: null,
|
|
204
|
+
totalTokens: null,
|
|
205
|
+
firstTokenLatencyMs: null,
|
|
206
|
+
durationMs: null,
|
|
207
|
+
safeErrorCode: null,
|
|
208
|
+
startedAt: "2026-07-31T00:00:00.000Z",
|
|
209
|
+
finishedAt: null,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const detail = await getAiRuntimeRun("run_1");
|
|
215
|
+
|
|
216
|
+
expect(detail.attemptItems).toEqual([
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
deploymentKey: "ai.primary",
|
|
219
|
+
requestedModel: "requested-model",
|
|
220
|
+
reportedModel: "provider-model",
|
|
221
|
+
}),
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
111
224
|
});
|
|
@@ -75,6 +75,12 @@ GET /api/system/ai-runtime/actors
|
|
|
75
75
|
opaque handle。报表与列表读取写入关键审计 `agent.runtime.report.read`,详情读取写入
|
|
76
76
|
`agent.runtime.run.read`。
|
|
77
77
|
|
|
78
|
+
运行列表的 `modelDeployments` 按组合首次出现的 Attempt 顺序,返回每个实际
|
|
79
|
+
`deploymentKey + model` 与出现次数;模型优先使用 Provider 回报,未回报时才保留请求
|
|
80
|
+
模型。该列表不折叠为“第一个模型等 N 个”,以便管理面在不依赖图表 hover 的情况下直接
|
|
81
|
+
读取当前范围内的模型组合。详情中每个 Attempt 同时保留 `sequence`、`deploymentKey`、
|
|
82
|
+
`requestedModel` 与可为空的 `reportedModel`,不会把实际回报伪装成请求值。
|
|
83
|
+
|
|
78
84
|
ADK Web 的 canonical bridge 仍以专用调试服务身份调用平台,但报表归属使用
|
|
79
85
|
`toModuleActor(context.actor)` 后的真实数据用户,使开发调试用量进入正确的业务范围,
|
|
80
86
|
而不是泄露或冒充普通登录会话。
|
|
@@ -78,3 +78,20 @@ result 通过上述投影后,才能写入聊天历史或业务成果。
|
|
|
78
78
|
删除对象包括业务仓库复制的通用 Supervisor Prompt、递归 `text`/JSON 解包器、任意节点
|
|
79
79
|
答案猜测和新 Run 路径上的 legacy fallback。业务专属 Prompt、权限、数据范围、会话与产品
|
|
80
80
|
投影不属于临时兼容层。
|
|
81
|
+
|
|
82
|
+
## 发布与下游验收证据
|
|
83
|
+
|
|
84
|
+
本契约随 `create-windy@0.6.10`、`@southwind-ai/modules@0.2.5`、
|
|
85
|
+
`@southwind-ai/server-sdk@0.4.1` 与 `@southwind-ai/ai-client@0.2.1` 发布。冻结提交
|
|
86
|
+
`1916707e888868d817a115dc8a96244a28e41d2c` 的完整候选通过 Linux x64、PostgreSQL 17、
|
|
87
|
+
四档生成、容器、最低基线升级和 package consumer;发布后 registry tarball 与候选字节一致。
|
|
88
|
+
|
|
89
|
+
sharp-sword 从 `0.6.9` 通过官方事务升级到 `0.6.10`,最终计划 54 个文件、0 冲突,并在
|
|
90
|
+
提交 `962b85cb21cf3d2e2efdc41ae768346da6b681e1` 删除 project-owned 递归 `text`/`total`
|
|
91
|
+
解包器。聚焦 Server/Domain/平台测试 44 项、Web 测试 66 项通过;真实 canonical Run
|
|
92
|
+
`run_7e731a6723b540b491f81fb301bde767` 完成并从 `typed-v1` 投影回答。升级后 doctor 为
|
|
93
|
+
0 missing;2 个 modified 仅是保留项目说明的 shared-scaffold 文档。
|
|
94
|
+
|
|
95
|
+
既有 `analysis.copilot.page-understanding` Operation Key 长于平台 helper 当前 32 字符上限,
|
|
96
|
+
因此保留原 Handler 注册以维持稳定 Key,但其输出已使用 typed Schema,消费链不再保留递归
|
|
97
|
+
兼容。该限制不能通过改名已发布 Operation Key 或伪装 helper 注册规避。
|
|
@@ -93,6 +93,18 @@ export interface AiRuntimeRunUsage {
|
|
|
93
93
|
totalTokens: number | null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 一个 Run 中实际观察到的模型与 deployment 组合。
|
|
98
|
+
*
|
|
99
|
+
* `model` 优先采用 Provider 回报的模型名;Provider 未回报时才保留请求模型。
|
|
100
|
+
* 排序遵循该组合首次出现的 Attempt 顺序,避免在报表投影时伪造执行顺序。
|
|
101
|
+
*/
|
|
102
|
+
export interface AiRuntimeRunModelDeployment {
|
|
103
|
+
deploymentKey: string;
|
|
104
|
+
model: string;
|
|
105
|
+
attemptCount: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
export interface AiRuntimeRunListItem {
|
|
97
109
|
runId: string;
|
|
98
110
|
operationKey: string;
|
|
@@ -102,6 +114,8 @@ export interface AiRuntimeRunListItem {
|
|
|
102
114
|
finishedAt: ISODateTime | null;
|
|
103
115
|
durationMs: number | null;
|
|
104
116
|
attemptCount: number;
|
|
117
|
+
modelDeployments: AiRuntimeRunModelDeployment[];
|
|
118
|
+
/** @deprecated 请使用包含 deployment 的 modelDeployments。 */
|
|
105
119
|
models: string[];
|
|
106
120
|
cacheStatus: AiRuntimeCacheStatus;
|
|
107
121
|
usage: AiRuntimeRunUsage;
|
|
@@ -112,6 +126,11 @@ export interface AiRuntimeAttemptDetail extends AiRuntimeRunUsage {
|
|
|
112
126
|
sequence: number;
|
|
113
127
|
toolRound: number;
|
|
114
128
|
deploymentKey: string;
|
|
129
|
+
/** 请求发送给 Provider 的模型名。 */
|
|
130
|
+
requestedModel: string;
|
|
131
|
+
/** Provider 响应中实际回报的模型名;缺失时不能推断。 */
|
|
132
|
+
reportedModel: string | null;
|
|
133
|
+
/** @deprecated 优先使用 reportedModel,缺失时使用 requestedModel。 */
|
|
115
134
|
model: string;
|
|
116
135
|
outcome: AiRuntimeAttemptOutcome;
|
|
117
136
|
cacheStatus: AiRuntimeCacheStatus;
|