@ryanfw/prompt-orchestration-pipeline 1.3.4 → 1.3.5
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/docs/http-api.md +3 -0
- package/docs/pop-task-guide.md +8 -0
- package/package.json +5 -2
- package/src/cli/__tests__/analyze-task.test.ts +1 -2
- package/src/config/__tests__/config-schema.test.ts +42 -0
- package/src/config/__tests__/legacy-literal-guard.test.ts +156 -0
- package/src/config/__tests__/models.test.ts +139 -1
- package/src/config/models.ts +121 -19
- package/src/core/__tests__/agent-step.test.ts +52 -0
- package/src/core/__tests__/job-view.test.ts +98 -0
- package/src/core/agent-step.ts +5 -1
- package/src/core/agent-types.ts +6 -1
- package/src/core/job-view.ts +21 -1
- package/src/llm/__tests__/dispatch.test.ts +130 -0
- package/src/llm/__tests__/index.test.ts +17 -18
- package/src/llm/__tests__/legacy-normalization.test.ts +109 -0
- package/src/llm/index.ts +109 -112
- package/src/providers/__tests__/claude-code.test.ts +9 -3
- package/src/providers/__tests__/moonshot.test.ts +33 -3
- package/src/providers/__tests__/types.test.ts +43 -0
- package/src/providers/__tests__/zhipu.test.ts +24 -28
- package/src/providers/claude-code.ts +2 -2
- package/src/providers/moonshot.ts +7 -0
- package/src/providers/types.ts +7 -5
- package/src/providers/zhipu.ts +0 -2
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +36 -1
- package/src/task-analysis/analyzer.ts +4 -10
- package/src/task-analysis/enrichers/schema-deducer.ts +8 -9
- package/src/ui/dist/assets/{index-DN3-zvtP.js → index-lBbVeNZC.js} +5 -2
- package/src/ui/dist/assets/{index-DN3-zvtP.js.map → index-lBbVeNZC.js.map} +1 -1
- package/src/ui/dist/index.html +1 -1
- package/src/ui/pages/Code.tsx +5 -2
- package/src/ui/server/__tests__/http-api-contract.test.ts +838 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +134 -27
- package/src/ui/server/__tests__/job-control-service.test.ts +282 -0
- package/src/ui/server/__tests__/job-process-registry.test.ts +151 -0
- package/src/ui/server/__tests__/job-repository.test.ts +142 -0
- package/src/ui/server/config-bridge.ts +9 -6
- package/src/ui/server/embedded-assets.ts +4 -4
- package/src/ui/server/endpoints/gate-endpoints.ts +24 -12
- package/src/ui/server/endpoints/job-control-endpoints.ts +19 -763
- package/src/ui/server/job-control-service.ts +520 -0
- package/src/ui/server/job-process-registry.ts +250 -0
- package/src/ui/server/job-repository.ts +316 -0
|
@@ -1196,6 +1196,104 @@ describe("normalizeJobView", () => {
|
|
|
1196
1196
|
});
|
|
1197
1197
|
});
|
|
1198
1198
|
});
|
|
1199
|
+
|
|
1200
|
+
describe("tokenUsage provider key normalization (AC-8)", () => {
|
|
1201
|
+
test('rewrites a persisted "zhipu:<model>" key to "zai:<model>"', () => {
|
|
1202
|
+
// Arrange / Act
|
|
1203
|
+
const view: JobView = normalizeJobView(
|
|
1204
|
+
{
|
|
1205
|
+
state: "done",
|
|
1206
|
+
tasks: { alpha: { state: "done", tokenUsage: [["zhipu:glm-5", 10, 5, 0.25]] } },
|
|
1207
|
+
},
|
|
1208
|
+
JOB_ID,
|
|
1209
|
+
LOCATION,
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
// Assert
|
|
1213
|
+
expect(view.tasks.alpha?.tokenUsage).toEqual([["zai:glm-5", 10, 5, 0.25]]);
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
test('rewrites a persisted "claudecode:<model>" key to "claude-code:<model>"', () => {
|
|
1217
|
+
// Arrange / Act
|
|
1218
|
+
const view: JobView = normalizeJobView(
|
|
1219
|
+
{
|
|
1220
|
+
state: "done",
|
|
1221
|
+
tasks: { alpha: { state: "done", tokenUsage: [["claudecode:sonnet", 1, 2, 0]] } },
|
|
1222
|
+
},
|
|
1223
|
+
JOB_ID,
|
|
1224
|
+
LOCATION,
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
// Assert
|
|
1228
|
+
expect(view.tasks.alpha?.tokenUsage).toEqual([["claude-code:sonnet", 1, 2, 0]]);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
test("preserves an unknown provider segment unchanged", () => {
|
|
1232
|
+
// Arrange / Act
|
|
1233
|
+
const view: JobView = normalizeJobView(
|
|
1234
|
+
{
|
|
1235
|
+
state: "done",
|
|
1236
|
+
tasks: { alpha: { state: "done", tokenUsage: [["unknown:model", 1, 1, 1]] } },
|
|
1237
|
+
},
|
|
1238
|
+
JOB_ID,
|
|
1239
|
+
LOCATION,
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
// Assert
|
|
1243
|
+
expect(view.tasks.alpha?.tokenUsage).toEqual([["unknown:model", 1, 1, 1]]);
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
test("preserves a colonless first element unchanged", () => {
|
|
1247
|
+
// Arrange / Act
|
|
1248
|
+
const view: JobView = normalizeJobView(
|
|
1249
|
+
{
|
|
1250
|
+
state: "done",
|
|
1251
|
+
tasks: { alpha: { state: "done", tokenUsage: [["bad"]] } },
|
|
1252
|
+
},
|
|
1253
|
+
JOB_ID,
|
|
1254
|
+
LOCATION,
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
// Assert
|
|
1258
|
+
expect(view.tasks.alpha?.tokenUsage).toEqual([["bad"]]);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
test("normalizes only the first colon-delimited segment", () => {
|
|
1262
|
+
// Arrange / Act
|
|
1263
|
+
const view: JobView = normalizeJobView(
|
|
1264
|
+
{
|
|
1265
|
+
state: "done",
|
|
1266
|
+
tasks: { alpha: { state: "done", tokenUsage: [["zhipu:glm-5:extra", 1, 1, 1]] } },
|
|
1267
|
+
},
|
|
1268
|
+
JOB_ID,
|
|
1269
|
+
LOCATION,
|
|
1270
|
+
);
|
|
1271
|
+
|
|
1272
|
+
// Assert
|
|
1273
|
+
expect(view.tasks.alpha?.tokenUsage).toEqual([["zai:glm-5:extra", 1, 1, 1]]);
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
test("cost totals match the pre-normalization values for a legacy key", () => {
|
|
1277
|
+
// Arrange / Act
|
|
1278
|
+
const view: JobView = normalizeJobView(
|
|
1279
|
+
{
|
|
1280
|
+
state: "done",
|
|
1281
|
+
tasks: { alpha: { state: "done", tokenUsage: [["zhipu:glm-5", 10, 5, 0.25]] } },
|
|
1282
|
+
},
|
|
1283
|
+
JOB_ID,
|
|
1284
|
+
LOCATION,
|
|
1285
|
+
);
|
|
1286
|
+
|
|
1287
|
+
// Assert — the numeric aggregation is independent of the key normalization.
|
|
1288
|
+
expect(view.costs).toMatchObject({
|
|
1289
|
+
totalCost: 0.25,
|
|
1290
|
+
totalTokens: 15,
|
|
1291
|
+
totalInputTokens: 10,
|
|
1292
|
+
totalOutputTokens: 5,
|
|
1293
|
+
});
|
|
1294
|
+
expect(view.tasks.alpha?.tokenUsage?.[0]).toEqual(["zai:glm-5", 10, 5, 0.25]);
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1199
1297
|
});
|
|
1200
1298
|
|
|
1201
1299
|
describe("deriveProgress (AC-6)", () => {
|
package/src/core/agent-step.ts
CHANGED
|
@@ -292,7 +292,10 @@ interface AgentRunDeps {
|
|
|
292
292
|
export async function executeAgent(
|
|
293
293
|
args: {
|
|
294
294
|
io: TaskFileIO;
|
|
295
|
-
entry: AgentEntryConfig & {
|
|
295
|
+
entry: AgentEntryConfig & {
|
|
296
|
+
name: string;
|
|
297
|
+
onEvent?: (event: HarnessEvent) => void;
|
|
298
|
+
};
|
|
296
299
|
},
|
|
297
300
|
deps?: AgentRunDeps,
|
|
298
301
|
): Promise<AgentStepResult> {
|
|
@@ -347,6 +350,7 @@ export async function executeAgent(
|
|
|
347
350
|
void io.writeLog(logName, JSON.stringify(redacted) + "\n", {
|
|
348
351
|
mode: "append",
|
|
349
352
|
});
|
|
353
|
+
entry.onEvent?.(event);
|
|
350
354
|
},
|
|
351
355
|
};
|
|
352
356
|
|
package/src/core/agent-types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HarnessName, ModelPayload, Usage } from "../harness/index.ts";
|
|
1
|
+
import type { HarnessEvent, HarnessName, ModelPayload, Usage } from "../harness/index.ts";
|
|
2
2
|
|
|
3
3
|
export interface McpServerConnection {
|
|
4
4
|
url: string;
|
|
@@ -54,6 +54,11 @@ export interface TaskAgentOptions {
|
|
|
54
54
|
idleTimeoutMs?: number;
|
|
55
55
|
/** Capture a git diff of the working tree as an `agent.patch` artifact. */
|
|
56
56
|
captureDiff?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Invoked for every adapter {@link HarnessEvent} observed during the run,
|
|
59
|
+
* after POP's own buffer + debug-log write. Absent, behavior is unchanged.
|
|
60
|
+
*/
|
|
61
|
+
onEvent?: (event: HarnessEvent) => void;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/** The `runAgent()` function injected into JavaScript task stages. */
|
package/src/core/job-view.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeLegacyProvider } from "../config/models.ts";
|
|
1
2
|
import {
|
|
2
3
|
JOB_STATUS_SYNONYMS,
|
|
3
4
|
VALID_JOB_STATUSES,
|
|
@@ -145,6 +146,25 @@ function toTaskError(value: unknown): JobViewTask["error"] {
|
|
|
145
146
|
return undefined;
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
function normalizeTokenUsageEntry(entry: unknown): unknown {
|
|
150
|
+
if (!Array.isArray(entry)) return entry;
|
|
151
|
+
const first = entry[0];
|
|
152
|
+
if (typeof first !== "string") return entry;
|
|
153
|
+
const colonIndex = first.indexOf(":");
|
|
154
|
+
if (colonIndex === -1) return entry;
|
|
155
|
+
const providerSegment = first.slice(0, colonIndex);
|
|
156
|
+
const canonical = normalizeLegacyProvider(providerSegment);
|
|
157
|
+
if (canonical === null) return entry;
|
|
158
|
+
const copy = entry.slice();
|
|
159
|
+
copy[0] = `${canonical}:${first.slice(colonIndex + 1)}`;
|
|
160
|
+
return copy;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeTokenUsage(value: unknown): unknown[] | undefined {
|
|
164
|
+
if (!Array.isArray(value)) return undefined;
|
|
165
|
+
return value.map(normalizeTokenUsageEntry);
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
function toGateInfo(value: unknown): GateInfo | null | undefined {
|
|
149
169
|
if (value === null) return null;
|
|
150
170
|
if (!isRecord(value)) return undefined;
|
|
@@ -189,7 +209,7 @@ function toJobViewTask(name: string, value: unknown): JobViewTask {
|
|
|
189
209
|
currentStage: typeof task["currentStage"] === "string" ? task["currentStage"] : undefined,
|
|
190
210
|
failedStage: typeof task["failedStage"] === "string" ? task["failedStage"] : undefined,
|
|
191
211
|
artifacts: task["artifacts"],
|
|
192
|
-
tokenUsage:
|
|
212
|
+
tokenUsage: normalizeTokenUsage(task["tokenUsage"]),
|
|
193
213
|
error: toTaskError(task["error"]),
|
|
194
214
|
skipReason: typeof task["skipReason"] === "string" ? task["skipReason"] : undefined,
|
|
195
215
|
skippedBy: typeof task["skippedBy"] === "string" ? task["skippedBy"] : undefined,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mock } from "bun:test";
|
|
3
|
+
import type { ChatOptions } from "../../providers/types.ts";
|
|
4
|
+
|
|
5
|
+
let moonshotReceived: Record<string, unknown> | undefined;
|
|
6
|
+
let claudeCodeReceived: Record<string, unknown> | undefined;
|
|
7
|
+
|
|
8
|
+
const moonshotChatMock = vi.fn(async (opts: ChatOptions) => {
|
|
9
|
+
moonshotReceived = opts as unknown as Record<string, unknown>;
|
|
10
|
+
return {
|
|
11
|
+
content: "moonshot-response",
|
|
12
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const claudeCodeChatMock = vi.fn(async (opts: ChatOptions) => {
|
|
17
|
+
claudeCodeReceived = opts as unknown as Record<string, unknown>;
|
|
18
|
+
return { content: "claude-code-response" };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
mock.module("../../providers/moonshot.ts", () => ({
|
|
22
|
+
moonshotChat: moonshotChatMock,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../../providers/claude-code.ts", () => ({
|
|
26
|
+
claudeCodeChat: claudeCodeChatMock,
|
|
27
|
+
isClaudeCodeAvailable: () => false,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
import { chat, registerMockProvider } from "../index.ts";
|
|
31
|
+
import type { MockProvider } from "../../providers/types.ts";
|
|
32
|
+
|
|
33
|
+
const baseMessages = [{ role: "user" as const, content: "hi" }];
|
|
34
|
+
|
|
35
|
+
describe("registry dispatch", () => {
|
|
36
|
+
it("moonshot forwards only declared sampling params (temperature, topP, stop)", async () => {
|
|
37
|
+
moonshotChatMock.mockClear();
|
|
38
|
+
await chat({
|
|
39
|
+
provider: "moonshot",
|
|
40
|
+
messages: baseMessages,
|
|
41
|
+
model: "kimi-k2.5",
|
|
42
|
+
temperature: 0.5,
|
|
43
|
+
topP: 0.9,
|
|
44
|
+
stop: ["end"],
|
|
45
|
+
frequencyPenalty: 1.0,
|
|
46
|
+
presencePenalty: 0.5,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(moonshotReceived).toBeDefined();
|
|
50
|
+
const opts = moonshotReceived!;
|
|
51
|
+
expect(opts.temperature).toBe(0.5);
|
|
52
|
+
expect(opts.topP).toBe(0.9);
|
|
53
|
+
expect(opts.stop).toEqual(["end"]);
|
|
54
|
+
expect(opts.frequencyPenalty).toBeUndefined();
|
|
55
|
+
expect(opts.presencePenalty).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("claude-code (CLI) forwards no sampling params", async () => {
|
|
59
|
+
claudeCodeChatMock.mockClear();
|
|
60
|
+
await chat({
|
|
61
|
+
provider: "claude-code",
|
|
62
|
+
messages: baseMessages,
|
|
63
|
+
model: "sonnet",
|
|
64
|
+
temperature: 0.5,
|
|
65
|
+
topP: 0.9,
|
|
66
|
+
stop: ["x"],
|
|
67
|
+
frequencyPenalty: 1.0,
|
|
68
|
+
presencePenalty: 0.5,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(claudeCodeReceived).toBeDefined();
|
|
72
|
+
const opts = claudeCodeReceived!;
|
|
73
|
+
expect(opts.temperature).toBeUndefined();
|
|
74
|
+
expect(opts.topP).toBeUndefined();
|
|
75
|
+
expect(opts.stop).toBeUndefined();
|
|
76
|
+
expect(opts.frequencyPenalty).toBeUndefined();
|
|
77
|
+
expect(opts.presencePenalty).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("forwards common fields to the adapter", async () => {
|
|
81
|
+
moonshotChatMock.mockClear();
|
|
82
|
+
await chat({
|
|
83
|
+
provider: "moonshot",
|
|
84
|
+
messages: baseMessages,
|
|
85
|
+
model: "kimi-k2.5",
|
|
86
|
+
maxTokens: 4096,
|
|
87
|
+
maxRetries: 2,
|
|
88
|
+
requestTimeoutMs: 30_000,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(moonshotReceived).toBeDefined();
|
|
92
|
+
const opts = moonshotReceived!;
|
|
93
|
+
expect(opts.messages).toEqual(baseMessages);
|
|
94
|
+
expect(opts.model).toBe("kimi-k2.5");
|
|
95
|
+
expect(opts.maxTokens).toBe(4096);
|
|
96
|
+
expect(opts.maxRetries).toBe(2);
|
|
97
|
+
expect(opts.requestTimeoutMs).toBe(30_000);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("mock dispatches through registered mockProvider with common fields", async () => {
|
|
101
|
+
const mockFn = vi.fn().mockResolvedValue({
|
|
102
|
+
content: { mock: true },
|
|
103
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
104
|
+
});
|
|
105
|
+
registerMockProvider({ chat: mockFn } as MockProvider);
|
|
106
|
+
|
|
107
|
+
await chat({
|
|
108
|
+
provider: "mock",
|
|
109
|
+
messages: baseMessages,
|
|
110
|
+
model: "test-model",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
114
|
+
const received = mockFn.mock.calls[0]![0] as Record<string, unknown>;
|
|
115
|
+
expect(received.provider).toBe("mock");
|
|
116
|
+
expect(received.messages).toEqual(baseMessages);
|
|
117
|
+
expect(received.model).toBe("test-model");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("throws for unknown provider before dispatch", async () => {
|
|
121
|
+
await expect(
|
|
122
|
+
chat({ provider: "nonexistent" as "mock", messages: baseMessages }),
|
|
123
|
+
).rejects.toThrow(/unknown provider/i);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ADAPTERS type-level exhaustiveness: ADAPTERS in src/llm/index.ts is typed
|
|
128
|
+
// `Record<ProviderName, AdapterFn>`. Adding a new ProviderName without an
|
|
129
|
+
// ADAPTERS row fails `bun run typecheck` because the mapped type requires every
|
|
130
|
+
// union member. This is a compile-time guarantee — no runtime test needed.
|
|
@@ -439,10 +439,10 @@ describe("LLM Gateway", () => {
|
|
|
439
439
|
}
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
-
it("returns false for
|
|
442
|
+
it("returns false for claude-code", () => {
|
|
443
443
|
expect(
|
|
444
|
-
resolveUsageAvailable("
|
|
445
|
-
provider: "
|
|
444
|
+
resolveUsageAvailable("claude-code", {
|
|
445
|
+
provider: "claude-code",
|
|
446
446
|
messages: baseMessages,
|
|
447
447
|
}),
|
|
448
448
|
).toBe(false);
|
|
@@ -545,7 +545,7 @@ describe("LLM Gateway", () => {
|
|
|
545
545
|
expect(cost).toBe(0);
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
-
it("
|
|
548
|
+
it("prices claude-code provider", () => {
|
|
549
549
|
// claude-code:sonnet — tokenCostInPerMillion: 0, tokenCostOutPerMillion: 0
|
|
550
550
|
const usage: NormalizedUsage = {
|
|
551
551
|
promptTokens: 1000,
|
|
@@ -553,7 +553,7 @@ describe("LLM Gateway", () => {
|
|
|
553
553
|
totalTokens: 2000,
|
|
554
554
|
source: "reported",
|
|
555
555
|
};
|
|
556
|
-
const cost = calculateCost("
|
|
556
|
+
const cost = calculateCost("claude-code", "sonnet", usage);
|
|
557
557
|
expect(cost).toBe(0);
|
|
558
558
|
});
|
|
559
559
|
});
|
|
@@ -570,18 +570,18 @@ describe("LLM Gateway", () => {
|
|
|
570
570
|
});
|
|
571
571
|
});
|
|
572
572
|
|
|
573
|
-
it("returns the zero-cost entry for
|
|
573
|
+
it("returns the zero-cost entry for claude-code", () => {
|
|
574
574
|
// claude-code:sonnet — zero-cost subscription
|
|
575
|
-
const price = findModelPrice("
|
|
575
|
+
const price = findModelPrice("claude-code", "sonnet");
|
|
576
576
|
expect(price).toEqual({
|
|
577
577
|
tokenCostInPerMillion: 0,
|
|
578
578
|
tokenCostOutPerMillion: 0,
|
|
579
579
|
});
|
|
580
580
|
});
|
|
581
581
|
|
|
582
|
-
it("returns the price for
|
|
583
|
-
//
|
|
584
|
-
const price = findModelPrice("
|
|
582
|
+
it("returns the price for zai", () => {
|
|
583
|
+
// zai:glm-5 → tokenCostInPerMillion: 1, tokenCostOutPerMillion: 3.2
|
|
584
|
+
const price = findModelPrice("zai", "glm-5");
|
|
585
585
|
expect(price).toEqual({
|
|
586
586
|
tokenCostInPerMillion: 1,
|
|
587
587
|
tokenCostOutPerMillion: 3.2,
|
|
@@ -874,10 +874,10 @@ describe("LLM Gateway", () => {
|
|
|
874
874
|
// Should have provider groups
|
|
875
875
|
expect(typeof llm).toBe("object");
|
|
876
876
|
|
|
877
|
-
// Check that known providers exist under the
|
|
877
|
+
// Check that known providers exist under the canonical gateway names.
|
|
878
878
|
expect(llm["openai"]).toBeDefined();
|
|
879
879
|
expect(llm["anthropic"]).toBeDefined();
|
|
880
|
-
expect(llm["
|
|
880
|
+
expect(llm["claude-code"]).toBeDefined();
|
|
881
881
|
expect(llm["zai"]).toBeDefined();
|
|
882
882
|
|
|
883
883
|
// Each provider group should have callable functions
|
|
@@ -891,13 +891,14 @@ describe("LLM Gateway", () => {
|
|
|
891
891
|
}
|
|
892
892
|
});
|
|
893
893
|
|
|
894
|
-
it("
|
|
894
|
+
it("exposes canonical provider groups only (no legacy alias keys)", () => {
|
|
895
895
|
const llm = createLLM();
|
|
896
896
|
|
|
897
|
-
expect(llm["
|
|
898
|
-
expect(typeof llm["claudecode"]?.["sonnet"]).toBe("function");
|
|
897
|
+
expect(typeof llm["claude-code"]?.["sonnet"]).toBe("function");
|
|
899
898
|
expect(typeof llm["zai"]?.["glm5"]).toBe("function");
|
|
900
|
-
|
|
899
|
+
// Legacy spellings are gone from the model-map surface.
|
|
900
|
+
expect(llm["claudecode"]).toBeUndefined();
|
|
901
|
+
expect(llm["zhipu"]).toBeUndefined();
|
|
901
902
|
expect(llm["xai"]).toBeUndefined();
|
|
902
903
|
});
|
|
903
904
|
|
|
@@ -974,7 +975,6 @@ describe("LLM Gateway", () => {
|
|
|
974
975
|
expect(availability.gemini).toBe(false);
|
|
975
976
|
expect(availability.deepseek).toBe(false);
|
|
976
977
|
expect(availability.zai).toBe(false);
|
|
977
|
-
expect(availability.zhipu).toBe(false);
|
|
978
978
|
expect(availability.moonshot).toBe(false);
|
|
979
979
|
expect(availability.alibaba).toBe(false);
|
|
980
980
|
});
|
|
@@ -990,7 +990,6 @@ describe("LLM Gateway", () => {
|
|
|
990
990
|
expect(availability.openai).toBe(true);
|
|
991
991
|
expect(availability.anthropic).toBe(true);
|
|
992
992
|
expect(availability.zai).toBe(true);
|
|
993
|
-
expect(availability.zhipu).toBe(true);
|
|
994
993
|
expect(availability.alibaba).toBe(true);
|
|
995
994
|
});
|
|
996
995
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mock } from "bun:test";
|
|
3
|
+
import type { ChatOptions } from "../../providers/types.ts";
|
|
4
|
+
|
|
5
|
+
const zaiChatMock = vi.fn(async (_opts: ChatOptions) => ({
|
|
6
|
+
content: { ok: true },
|
|
7
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
mock.module("../../providers/zhipu.ts", () => ({
|
|
11
|
+
zaiChat: zaiChatMock,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module("../../core/config.ts", () => ({
|
|
15
|
+
getConfig: () => ({
|
|
16
|
+
llm: {
|
|
17
|
+
defaultProvider: "zhipu",
|
|
18
|
+
defaultModel: "glm-5",
|
|
19
|
+
internalProvider: "zai",
|
|
20
|
+
internalModel: "glm-5",
|
|
21
|
+
maxConcurrency: 5,
|
|
22
|
+
retryMaxAttempts: 3,
|
|
23
|
+
retryBackoffMs: 1000,
|
|
24
|
+
},
|
|
25
|
+
taskRunner: {
|
|
26
|
+
maxRefinementAttempts: 3,
|
|
27
|
+
stageTimeout: 300000,
|
|
28
|
+
llmRequestTimeout: 3600000,
|
|
29
|
+
maxAttempts: 3,
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
resetConfig: () => {},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
chat,
|
|
37
|
+
complete,
|
|
38
|
+
createLLMWithOverride,
|
|
39
|
+
findModelPrice,
|
|
40
|
+
} from "../index.ts";
|
|
41
|
+
|
|
42
|
+
const baseMessages = [{ role: "user" as const, content: "hi" }];
|
|
43
|
+
|
|
44
|
+
function lastCallProvider(): string {
|
|
45
|
+
const calls = zaiChatMock.mock.calls as unknown as Array<
|
|
46
|
+
[ChatOptions]
|
|
47
|
+
>;
|
|
48
|
+
return calls[0]![0].provider;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("legacy provider normalization (AC-4)", () => {
|
|
52
|
+
it("chat() dispatches legacy zhipu to the zai adapter", async () => {
|
|
53
|
+
zaiChatMock.mockClear();
|
|
54
|
+
|
|
55
|
+
await chat({
|
|
56
|
+
provider: "zhipu" as never,
|
|
57
|
+
messages: baseMessages,
|
|
58
|
+
model: "glm-5",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(zaiChatMock).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(lastCallProvider()).toBe("zai");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("complete() resolves a legacy zhipu defaultProvider to zai", async () => {
|
|
66
|
+
zaiChatMock.mockClear();
|
|
67
|
+
|
|
68
|
+
await complete("hello");
|
|
69
|
+
|
|
70
|
+
expect(zaiChatMock).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(lastCallProvider()).toBe("zai");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("createLLMWithOverride() resolves a legacy zhipu override to zai", async () => {
|
|
75
|
+
zaiChatMock.mockClear();
|
|
76
|
+
|
|
77
|
+
const llm = createLLMWithOverride({
|
|
78
|
+
provider: "zhipu" as never,
|
|
79
|
+
model: "glm-5",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await llm.chat({ provider: "openai", messages: baseMessages } as ChatOptions);
|
|
83
|
+
|
|
84
|
+
expect(zaiChatMock).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(lastCallProvider()).toBe("zai");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("findModelPrice() normalizes a persisted zhipu cost key to zai", () => {
|
|
89
|
+
const legacyPrice = findModelPrice("zhipu", "glm-5");
|
|
90
|
+
const canonicalPrice = findModelPrice("zai", "glm-5");
|
|
91
|
+
|
|
92
|
+
expect(legacyPrice).not.toBeNull();
|
|
93
|
+
expect(legacyPrice).toEqual(canonicalPrice);
|
|
94
|
+
expect(legacyPrice).toEqual({
|
|
95
|
+
tokenCostInPerMillion: 1,
|
|
96
|
+
tokenCostOutPerMillion: 3.2,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("chat() throws an explicit Unknown provider error before adapter dispatch", async () => {
|
|
101
|
+
zaiChatMock.mockClear();
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
chat({ provider: "neverheard" as never, messages: baseMessages }),
|
|
105
|
+
).rejects.toThrow("Unknown provider: neverheard");
|
|
106
|
+
|
|
107
|
+
expect(zaiChatMock).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|