@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
package/docs/http-api.md
CHANGED
|
@@ -81,7 +81,10 @@ or on error:
|
|
|
81
81
|
| `forbidden_origin` | 403 | Cross-origin mutation rejected |
|
|
82
82
|
| `status_unavailable` | 500 | Job status file unreadable |
|
|
83
83
|
| `no_pending_gate` | 409 | Job has no pending gate decision |
|
|
84
|
+
| `INVALID_JSON` | 409 | Job status JSON is invalid |
|
|
85
|
+
| `STATUS_CORRUPT` | 409 | Job status file is corrupt |
|
|
84
86
|
| `FS_ERROR` | 500 | Filesystem operation failed |
|
|
87
|
+
| `BAD_PATH` | 400 | Task analysis path is invalid |
|
|
85
88
|
| `BAD_REQUEST` | 400 | Malformed request body |
|
|
86
89
|
| `NOT_FOUND` | 404 | Route or resource not found |
|
|
87
90
|
|
package/docs/pop-task-guide.md
CHANGED
|
@@ -209,6 +209,7 @@ export const inference = async ({ runAgent, io, data, flags }) => {
|
|
|
209
209
|
// timeoutMs?: number // optional wall-clock cap
|
|
210
210
|
// idleTimeoutMs?: number // optional no-output cap
|
|
211
211
|
// captureDiff?: boolean // capture a git diff as 'agent.patch'
|
|
212
|
+
// onEvent?: (event) => {} // observe adapter harness events during the run
|
|
212
213
|
});
|
|
213
214
|
|
|
214
215
|
if (!result.ok) {
|
|
@@ -226,6 +227,13 @@ By default (`io` is `true`) the agent shares the task's file I/O: it can call th
|
|
|
226
227
|
task sees, and its `agent-result.md` is written automatically. Token usage and
|
|
227
228
|
cost flow into the job status like any other LLM call.
|
|
228
229
|
|
|
230
|
+
Use `onEvent` when a task needs to observe live harness activity, such as
|
|
231
|
+
assistant messages, tool calls, usage events, or run completion. POP invokes the
|
|
232
|
+
callback after it has buffered the event and appended the redacted debug log, so
|
|
233
|
+
omitting the callback leaves behavior unchanged. The `event.raw` payload comes
|
|
234
|
+
directly from the underlying CLI adapter; treat it as unstable and avoid
|
|
235
|
+
persisting it unless you have considered sensitive values in the raw stream.
|
|
236
|
+
|
|
229
237
|
**`runAgent` vs `llm`**: use `llm.<provider>.chat()` for a single request/response
|
|
230
238
|
LLM call; use `runAgent()` when you need a tool-using CLI agent that reads and
|
|
231
239
|
writes files over multiple turns.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server/index.ts",
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
"ui": "NODE_ENV=development bun --watch src/ui/server/index.ts",
|
|
33
33
|
"ui:dev": "vite",
|
|
34
34
|
"ui:build": "vite build && bun run generate:embedded-assets",
|
|
35
|
-
"
|
|
35
|
+
"verify:package": "bun scripts/verify-package-assets.js",
|
|
36
|
+
"smoke:packed-consumer": "bun scripts/smoke-packed-consumer.js",
|
|
37
|
+
"release:check": "bun run ui:build && bun run verify:package && bun run smoke:packed-consumer",
|
|
38
|
+
"prepack": "bun run ui:build && bun run verify:package",
|
|
36
39
|
"ui:prod": "bun src/ui/server/index.ts",
|
|
37
40
|
"demo:ui": "NODE_ENV=production PO_ROOT=demo bun src/ui/server/index.ts",
|
|
38
41
|
"demo:orchestrator": "PO_ROOT=demo NODE_ENV=production bun -e \"import('./src/core/orchestrator.ts').then(m => m.startOrchestrator({ dataDir: process.env.PO_ROOT || 'demo' })).catch(err => { console.error(err); process.exit(1) })\"",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import Ajv from "ajv";
|
|
5
|
+
|
|
6
|
+
const schemaPath = join(process.cwd(), "config.schema.json");
|
|
7
|
+
const schema = JSON.parse(readFileSync(schemaPath, "utf-8")) as {
|
|
8
|
+
properties: { llm: { properties: { defaultProvider: { enum: string[] } } } };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
12
|
+
const validate = ajv.compile(schema);
|
|
13
|
+
|
|
14
|
+
function llmConfig(defaultProvider: string): unknown {
|
|
15
|
+
return { llm: { defaultProvider } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("config.schema.json defaultProvider enum", () => {
|
|
19
|
+
it("accepts canonical provider ids", () => {
|
|
20
|
+
expect(validate(llmConfig("openai"))).toBe(true);
|
|
21
|
+
expect(validate(llmConfig("zai"))).toBe(true);
|
|
22
|
+
expect(validate(llmConfig("claude-code"))).toBe(true);
|
|
23
|
+
expect(validate(llmConfig("moonshot"))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rejects legacy provider ids", () => {
|
|
27
|
+
expect(validate(llmConfig("zhipu"))).toBe(false);
|
|
28
|
+
expect(validate(llmConfig("claudecode"))).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("rejects unknown provider ids", () => {
|
|
32
|
+
expect(validate(llmConfig("neverheard"))).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("enum matches the canonical ProviderName set (no legacy spellings)", () => {
|
|
36
|
+
const enumValues = schema.properties.llm.properties.defaultProvider.enum;
|
|
37
|
+
expect(enumValues).not.toContain("zhipu");
|
|
38
|
+
expect(enumValues).not.toContain("claudecode");
|
|
39
|
+
expect(enumValues).toContain("zai");
|
|
40
|
+
expect(enumValues).toContain("claude-code");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
const FORBIDDEN_LITERALS: ReadonlySet<string> = new Set([
|
|
6
|
+
"zhipu",
|
|
7
|
+
"claudecode",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const STRING_LITERAL_RE = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
|
|
11
|
+
|
|
12
|
+
const ALLOWED_FILES: ReadonlySet<string> = new Set([
|
|
13
|
+
"src/config/models.ts",
|
|
14
|
+
"src/config/__tests__/models.test.ts",
|
|
15
|
+
"src/config/__tests__/config-schema.test.ts",
|
|
16
|
+
"src/config/__tests__/legacy-literal-guard.test.ts",
|
|
17
|
+
"src/llm/__tests__/legacy-normalization.test.ts",
|
|
18
|
+
"src/llm/__tests__/index.test.ts",
|
|
19
|
+
"src/providers/__tests__/types.test.ts",
|
|
20
|
+
"src/task-analysis/__tests__/analyzer.test.ts",
|
|
21
|
+
"src/task-analysis/__tests__/enrichers-schema-deducer.test.ts",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export interface LegacyLiteralViolation {
|
|
25
|
+
path: string;
|
|
26
|
+
literal: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SourceFile {
|
|
30
|
+
path: string;
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function extractStringLiterals(source: string): string[] {
|
|
35
|
+
const out: string[] = [];
|
|
36
|
+
for (const match of source.matchAll(STRING_LITERAL_RE)) {
|
|
37
|
+
const raw = match[0] as string;
|
|
38
|
+
out.push(raw.slice(1, -1));
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function scanFiles(
|
|
44
|
+
files: SourceFile[],
|
|
45
|
+
allowlist: ReadonlySet<string>,
|
|
46
|
+
): LegacyLiteralViolation[] {
|
|
47
|
+
const violations: LegacyLiteralViolation[] = [];
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
if (allowlist.has(file.path)) continue;
|
|
50
|
+
for (const literal of extractStringLiterals(file.content)) {
|
|
51
|
+
if (FORBIDDEN_LITERALS.has(literal)) {
|
|
52
|
+
violations.push({ path: file.path, literal });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function listSourceTsFiles(scanRoot: string, base: string): SourceFile[] {
|
|
60
|
+
const rootAbs = resolve(scanRoot);
|
|
61
|
+
const baseAbs = resolve(base);
|
|
62
|
+
const out: SourceFile[] = [];
|
|
63
|
+
|
|
64
|
+
function walk(dir: string): void {
|
|
65
|
+
for (const entry of readdirSync(dir)) {
|
|
66
|
+
const full = join(dir, entry);
|
|
67
|
+
const st = statSync(full);
|
|
68
|
+
if (st.isDirectory()) {
|
|
69
|
+
walk(full);
|
|
70
|
+
} else if (st.isFile() && /\.(ts|tsx)$/.test(entry)) {
|
|
71
|
+
const rel = relative(baseAbs, full).split(sep).join("/");
|
|
72
|
+
out.push({ path: rel, content: readFileSync(full, "utf-8") });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
walk(rootAbs);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("extractStringLiterals", () => {
|
|
82
|
+
it("captures a double-quoted forbidden literal", () => {
|
|
83
|
+
expect(extractStringLiterals('const p = "zhipu";')).toContain("zhipu");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("ignores the bare legacy word in a comment", () => {
|
|
87
|
+
expect(extractStringLiterals("// legacy zhipu id\nconst x = 1;")).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("scanFiles — forbidden literal detection", () => {
|
|
92
|
+
it('flags "zhipu" outside the allowlist', () => {
|
|
93
|
+
const violations = scanFiles(
|
|
94
|
+
[{ path: "src/x.ts", content: 'const p = "zhipu";' }],
|
|
95
|
+
new Set(),
|
|
96
|
+
);
|
|
97
|
+
expect(violations).toEqual([{ path: "src/x.ts", literal: "zhipu" }]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('flags "claudecode" outside the allowlist', () => {
|
|
101
|
+
const violations = scanFiles(
|
|
102
|
+
[{ path: "src/y.ts", content: "const q = \"claudecode\";" }],
|
|
103
|
+
new Set(),
|
|
104
|
+
);
|
|
105
|
+
expect(violations).toEqual([{ path: "src/y.ts", literal: "claudecode" }]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("skips files listed in the allowlist", () => {
|
|
109
|
+
const violations = scanFiles(
|
|
110
|
+
[{ path: "src/config/models.ts", content: 'legacyAliases: ["zhipu"]' }],
|
|
111
|
+
new Set(["src/config/models.ts"]),
|
|
112
|
+
);
|
|
113
|
+
expect(violations).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("scanFiles — legitimate substrings are not flagged", () => {
|
|
118
|
+
it("ignores the ZHIPU_API_KEY env-var name", () => {
|
|
119
|
+
const violations = scanFiles(
|
|
120
|
+
[{ path: "src/env.ts", content: 'const k = "ZHIPU_API_KEY";' }],
|
|
121
|
+
new Set(),
|
|
122
|
+
);
|
|
123
|
+
expect(violations).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("ignores an import path that ends in zhipu.ts", () => {
|
|
127
|
+
const violations = scanFiles(
|
|
128
|
+
[
|
|
129
|
+
{
|
|
130
|
+
path: "src/llm/index.ts",
|
|
131
|
+
content: 'import { zaiChat } from "../providers/zhipu.ts";',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
new Set(),
|
|
135
|
+
);
|
|
136
|
+
expect(violations).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("ignores the legacy word inside a comment", () => {
|
|
140
|
+
const violations = scanFiles(
|
|
141
|
+
[{ path: "src/cmt.ts", content: "// legacy zhipu id\nconst x = 1;" }],
|
|
142
|
+
new Set(),
|
|
143
|
+
);
|
|
144
|
+
expect(violations).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("legacy-literal source guard (AC-3)", () => {
|
|
149
|
+
it("src/ has no forbidden legacy literals outside the allowlist", () => {
|
|
150
|
+
const cwd = process.cwd();
|
|
151
|
+
const files = listSourceTsFiles(join(cwd, "src"), cwd);
|
|
152
|
+
const violations = scanFiles(files, ALLOWED_FILES);
|
|
153
|
+
|
|
154
|
+
expect(violations).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -6,17 +6,20 @@ import {
|
|
|
6
6
|
DEFAULT_MODEL_BY_PROVIDER,
|
|
7
7
|
FUNCTION_NAME_BY_ALIAS,
|
|
8
8
|
PROVIDER_FUNCTIONS,
|
|
9
|
+
PROVIDERS,
|
|
9
10
|
aliasToFunctionName,
|
|
10
11
|
getProviderFromAlias,
|
|
11
12
|
getModelFromAlias,
|
|
12
13
|
getModelConfig,
|
|
13
14
|
buildProviderFunctionsIndex,
|
|
15
|
+
normalizeLegacyProvider,
|
|
14
16
|
validateModelRegistry,
|
|
15
17
|
} from "../models";
|
|
16
|
-
import type { ModelConfigEntry } from "../models";
|
|
18
|
+
import type { ModelConfigEntry, ProviderName, ProviderMeta } from "../models";
|
|
17
19
|
|
|
18
20
|
const MODEL_COUNT = 51;
|
|
19
21
|
const PROVIDER_COUNT = 9;
|
|
22
|
+
const REGISTRY_PROVIDER_COUNT = 10;
|
|
20
23
|
|
|
21
24
|
describe("ModelAlias", () => {
|
|
22
25
|
it(`has exactly ${MODEL_COUNT} entries`, () => {
|
|
@@ -378,3 +381,138 @@ describe("validateModelRegistry — invariant failures", () => {
|
|
|
378
381
|
expect(() => validateModelRegistry(config, set)).not.toThrow();
|
|
379
382
|
});
|
|
380
383
|
});
|
|
384
|
+
|
|
385
|
+
describe("normalizeLegacyProvider", () => {
|
|
386
|
+
it('maps legacy "zhipu" to canonical "zai"', () => {
|
|
387
|
+
expect(normalizeLegacyProvider("zhipu")).toBe("zai");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('maps legacy "claudecode" to canonical "claude-code"', () => {
|
|
391
|
+
expect(normalizeLegacyProvider("claudecode")).toBe("claude-code");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('returns canonical "zai" for itself', () => {
|
|
395
|
+
expect(normalizeLegacyProvider("zai")).toBe("zai");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('returns canonical "claude-code" for itself', () => {
|
|
399
|
+
expect(normalizeLegacyProvider("claude-code")).toBe("claude-code");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('returns canonical "openai" for itself', () => {
|
|
403
|
+
expect(normalizeLegacyProvider("openai")).toBe("openai");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("returns null for an unknown provider string", () => {
|
|
407
|
+
expect(normalizeLegacyProvider("unknown")).toBeNull();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("returns null for the empty string", () => {
|
|
411
|
+
expect(normalizeLegacyProvider("")).toBeNull();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("returns null for Object.prototype property names", () => {
|
|
415
|
+
expect(normalizeLegacyProvider("constructor")).toBeNull();
|
|
416
|
+
expect(normalizeLegacyProvider("toString")).toBeNull();
|
|
417
|
+
expect(normalizeLegacyProvider("valueOf")).toBeNull();
|
|
418
|
+
expect(normalizeLegacyProvider("hasOwnProperty")).toBeNull();
|
|
419
|
+
expect(normalizeLegacyProvider("__proto__")).toBeNull();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("PROVIDERS registry invariants", () => {
|
|
424
|
+
const CANONICAL_PROVIDER_IDS: ProviderName[] = [
|
|
425
|
+
"openai",
|
|
426
|
+
"anthropic",
|
|
427
|
+
"deepseek",
|
|
428
|
+
"gemini",
|
|
429
|
+
"zai",
|
|
430
|
+
"claude-code",
|
|
431
|
+
"moonshot",
|
|
432
|
+
"alibaba",
|
|
433
|
+
"opencode",
|
|
434
|
+
"mock",
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
it(`has exactly ${REGISTRY_PROVIDER_COUNT} rows, one per canonical ProviderName`, () => {
|
|
438
|
+
expect(Object.keys(PROVIDERS).length).toBe(REGISTRY_PROVIDER_COUNT);
|
|
439
|
+
for (const id of CANONICAL_PROVIDER_IDS) {
|
|
440
|
+
expect(id in PROVIDERS).toBe(true);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("table is frozen", () => {
|
|
445
|
+
expect(Object.isFrozen(PROVIDERS)).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("legacyAliases are unique across the registry (no alias maps to two providers)", () => {
|
|
449
|
+
const seen = new Map<string, ProviderName>();
|
|
450
|
+
for (const [canonical, meta] of Object.entries(PROVIDERS) as [ProviderName, ProviderMeta][]) {
|
|
451
|
+
for (const alias of meta.legacyAliases) {
|
|
452
|
+
const prior = seen.get(alias);
|
|
453
|
+
if (prior !== undefined) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
`legacy alias "${alias}" is claimed by both "${prior}" and "${canonical}"`,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
seen.set(alias, canonical);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("no legacy alias collides with a canonical id", () => {
|
|
464
|
+
const canonicalSet = new Set<string>(CANONICAL_PROVIDER_IDS);
|
|
465
|
+
for (const meta of Object.values(PROVIDERS)) {
|
|
466
|
+
for (const alias of meta.legacyAliases) {
|
|
467
|
+
expect(canonicalSet.has(alias)).toBe(false);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("MODEL_CONFIG providers are all registered catalog providers (no orphans, no mock)", () => {
|
|
473
|
+
for (const entry of Object.values(MODEL_CONFIG)) {
|
|
474
|
+
expect(entry.provider in PROVIDERS).toBe(true);
|
|
475
|
+
expect(entry.provider).not.toBe("mock");
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("MODEL_CONFIG / DEFAULT_MODEL_BY_PROVIDER / PROVIDER_FUNCTIONS exclude only mock from the registry", () => {
|
|
480
|
+
const registryKeys = new Set<string>(Object.keys(PROVIDERS));
|
|
481
|
+
const catalogKeys = new Set<string>(Object.keys(PROVIDER_FUNCTIONS));
|
|
482
|
+
const defaultKeys = new Set<string>(
|
|
483
|
+
Object.keys(DEFAULT_MODEL_BY_PROVIDER) as string[],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
for (const key of catalogKeys) expect(registryKeys.has(key)).toBe(true);
|
|
487
|
+
for (const key of defaultKeys) expect(registryKeys.has(key)).toBe(true);
|
|
488
|
+
|
|
489
|
+
const missingFromCatalog = [...registryKeys].filter(
|
|
490
|
+
(k) => !catalogKeys.has(k),
|
|
491
|
+
);
|
|
492
|
+
const missingFromDefaults = [...registryKeys].filter(
|
|
493
|
+
(k) => !defaultKeys.has(k),
|
|
494
|
+
);
|
|
495
|
+
expect(missingFromCatalog).toEqual(["mock"]);
|
|
496
|
+
expect(missingFromDefaults).toEqual(["mock"]);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("canonical<->alias round-trip via normalizeLegacyProvider", () => {
|
|
500
|
+
for (const [canonical, meta] of Object.entries(PROVIDERS) as [ProviderName, ProviderMeta][]) {
|
|
501
|
+
expect(normalizeLegacyProvider(canonical)).toBe(canonical);
|
|
502
|
+
for (const alias of meta.legacyAliases) {
|
|
503
|
+
expect(normalizeLegacyProvider(alias)).toBe(canonical);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("CLI providers declare sampling: []", () => {
|
|
509
|
+
expect(PROVIDERS["claude-code"]!.sampling).toEqual([]);
|
|
510
|
+
expect(PROVIDERS.opencode!.sampling).toEqual([]);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("mock is runtime-only: empty envKeys/sampling, isCLI false", () => {
|
|
514
|
+
expect(PROVIDERS.mock!.envKeys).toEqual([]);
|
|
515
|
+
expect(PROVIDERS.mock!.sampling).toEqual([]);
|
|
516
|
+
expect(PROVIDERS.mock!.isCLI).toBe(false);
|
|
517
|
+
});
|
|
518
|
+
});
|
package/src/config/models.ts
CHANGED
|
@@ -1,16 +1,118 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
| "
|
|
9
|
-
| "
|
|
10
|
-
| "
|
|
1
|
+
import type { ProviderName } from "../providers/types.ts";
|
|
2
|
+
|
|
3
|
+
export type { ProviderName };
|
|
4
|
+
|
|
5
|
+
// ─── Provider Facts Registry (pure data; behavior wired in llm/index.ts) ─────
|
|
6
|
+
|
|
7
|
+
export type SamplingParam =
|
|
8
|
+
| "temperature"
|
|
9
|
+
| "topP"
|
|
10
|
+
| "stop"
|
|
11
|
+
| "frequencyPenalty"
|
|
12
|
+
| "presencePenalty"
|
|
13
|
+
| "seed";
|
|
14
|
+
|
|
15
|
+
export interface ProviderMeta {
|
|
16
|
+
usageAvailable: boolean;
|
|
17
|
+
isCLI: boolean;
|
|
18
|
+
sampling: SamplingParam[];
|
|
19
|
+
envKeys: string[];
|
|
20
|
+
legacyAliases: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ModelCatalogProviderName = Exclude<ProviderName, "mock">;
|
|
24
|
+
|
|
25
|
+
export const PROVIDERS: Readonly<Record<ProviderName, ProviderMeta>> = Object.freeze({
|
|
26
|
+
openai: {
|
|
27
|
+
usageAvailable: true,
|
|
28
|
+
isCLI: false,
|
|
29
|
+
sampling: ["temperature", "topP", "stop", "frequencyPenalty", "presencePenalty", "seed"],
|
|
30
|
+
envKeys: ["OPENAI_API_KEY"],
|
|
31
|
+
legacyAliases: [],
|
|
32
|
+
},
|
|
33
|
+
anthropic: {
|
|
34
|
+
usageAvailable: true,
|
|
35
|
+
isCLI: false,
|
|
36
|
+
sampling: ["temperature", "topP", "stop"],
|
|
37
|
+
envKeys: ["ANTHROPIC_API_KEY"],
|
|
38
|
+
legacyAliases: [],
|
|
39
|
+
},
|
|
40
|
+
gemini: {
|
|
41
|
+
usageAvailable: true,
|
|
42
|
+
isCLI: false,
|
|
43
|
+
sampling: ["temperature", "topP", "stop"],
|
|
44
|
+
envKeys: ["GEMINI_API_KEY"],
|
|
45
|
+
legacyAliases: [],
|
|
46
|
+
},
|
|
47
|
+
deepseek: {
|
|
48
|
+
usageAvailable: true,
|
|
49
|
+
isCLI: false,
|
|
50
|
+
sampling: ["temperature", "topP", "stop", "frequencyPenalty", "presencePenalty"],
|
|
51
|
+
envKeys: ["DEEPSEEK_API_KEY"],
|
|
52
|
+
legacyAliases: [],
|
|
53
|
+
},
|
|
54
|
+
moonshot: {
|
|
55
|
+
usageAvailable: true,
|
|
56
|
+
isCLI: false,
|
|
57
|
+
sampling: ["temperature", "topP", "stop"],
|
|
58
|
+
envKeys: ["MOONSHOT_API_KEY"],
|
|
59
|
+
legacyAliases: [],
|
|
60
|
+
},
|
|
61
|
+
zai: {
|
|
62
|
+
usageAvailable: true,
|
|
63
|
+
isCLI: false,
|
|
64
|
+
sampling: ["temperature", "topP", "stop"],
|
|
65
|
+
envKeys: ["ZAI_API_KEY", "ZHIPU_API_KEY"],
|
|
66
|
+
legacyAliases: ["zhipu"],
|
|
67
|
+
},
|
|
68
|
+
alibaba: {
|
|
69
|
+
usageAvailable: true,
|
|
70
|
+
isCLI: false,
|
|
71
|
+
sampling: ["temperature", "topP", "stop", "frequencyPenalty", "presencePenalty"],
|
|
72
|
+
envKeys: ["ALIBABA_API_KEY"],
|
|
73
|
+
legacyAliases: [],
|
|
74
|
+
},
|
|
75
|
+
"claude-code": {
|
|
76
|
+
usageAvailable: false,
|
|
77
|
+
isCLI: true,
|
|
78
|
+
sampling: [],
|
|
79
|
+
envKeys: [],
|
|
80
|
+
legacyAliases: ["claudecode"],
|
|
81
|
+
},
|
|
82
|
+
opencode: {
|
|
83
|
+
usageAvailable: true,
|
|
84
|
+
isCLI: true,
|
|
85
|
+
sampling: [],
|
|
86
|
+
envKeys: [],
|
|
87
|
+
legacyAliases: [],
|
|
88
|
+
},
|
|
89
|
+
mock: {
|
|
90
|
+
usageAvailable: true,
|
|
91
|
+
isCLI: false,
|
|
92
|
+
sampling: [],
|
|
93
|
+
envKeys: [],
|
|
94
|
+
legacyAliases: [],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves a runtime provider string to its canonical ProviderName. Returns the
|
|
100
|
+
* canonical id for canonical input and for registered legacy aliases; returns
|
|
101
|
+
* `null` for unknown strings. Derived from PROVIDERS[*].legacyAliases so the
|
|
102
|
+
* alias map stays single-source.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeLegacyProvider(name: string): ProviderName | null {
|
|
105
|
+
if (Object.hasOwn(PROVIDERS, name)) return name as ProviderName;
|
|
106
|
+
for (const [canonical, meta] of Object.entries(PROVIDERS) as [ProviderName, ProviderMeta][]) {
|
|
107
|
+
if (meta.legacyAliases.includes(name)) return canonical;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Model Catalog Types ─────────────────────────────────────────────────────
|
|
11
113
|
|
|
12
114
|
export interface ModelConfigEntry {
|
|
13
|
-
readonly provider:
|
|
115
|
+
readonly provider: ModelCatalogProviderName;
|
|
14
116
|
readonly model: string;
|
|
15
117
|
readonly tokenCostInPerMillion: number;
|
|
16
118
|
readonly tokenCostOutPerMillion: number;
|
|
@@ -18,7 +120,7 @@ export interface ModelConfigEntry {
|
|
|
18
120
|
|
|
19
121
|
export interface ProviderFunctionEntry {
|
|
20
122
|
readonly alias: string;
|
|
21
|
-
readonly provider:
|
|
123
|
+
readonly provider: ModelCatalogProviderName;
|
|
22
124
|
readonly model: string;
|
|
23
125
|
readonly functionName: string;
|
|
24
126
|
readonly fullPath: string;
|
|
@@ -452,7 +554,7 @@ export function aliasToFunctionName(alias: string): string {
|
|
|
452
554
|
);
|
|
453
555
|
}
|
|
454
556
|
|
|
455
|
-
export function getProviderFromAlias(alias: string):
|
|
557
|
+
export function getProviderFromAlias(alias: string): ModelCatalogProviderName {
|
|
456
558
|
if (typeof alias !== "string") {
|
|
457
559
|
throw new Error(
|
|
458
560
|
`Invalid model alias: expected string, got ${typeof alias}`,
|
|
@@ -461,7 +563,7 @@ export function getProviderFromAlias(alias: string): ProviderName {
|
|
|
461
563
|
if (!alias.includes(":")) {
|
|
462
564
|
throw new Error(`Invalid model alias: "${alias}" does not contain a colon`);
|
|
463
565
|
}
|
|
464
|
-
return alias.split(":")[0] as
|
|
566
|
+
return alias.split(":")[0] as ModelCatalogProviderName;
|
|
465
567
|
}
|
|
466
568
|
|
|
467
569
|
export function getModelFromAlias(alias: string): string {
|
|
@@ -486,7 +588,7 @@ export function getModelConfig(alias: string): ModelConfigEntry | null {
|
|
|
486
588
|
// ─── Default Model By Provider ───────────────────────────────────────────────
|
|
487
589
|
|
|
488
590
|
export const DEFAULT_MODEL_BY_PROVIDER: Readonly<
|
|
489
|
-
Record<
|
|
591
|
+
Record<ModelCatalogProviderName, ModelAliasKey>
|
|
490
592
|
> = Object.freeze({
|
|
491
593
|
openai: "openai:gpt-5.4",
|
|
492
594
|
anthropic: "anthropic:sonnet-4-6",
|
|
@@ -497,7 +599,7 @@ export const DEFAULT_MODEL_BY_PROVIDER: Readonly<
|
|
|
497
599
|
zai: "zai:glm-5-1",
|
|
498
600
|
alibaba: "alibaba:qwen3-max",
|
|
499
601
|
opencode: "opencode:default",
|
|
500
|
-
}
|
|
602
|
+
} satisfies Record<ModelCatalogProviderName, ModelAliasKey>);
|
|
501
603
|
|
|
502
604
|
// ─── Function Name Derived Index ─────────────────────────────────────────────
|
|
503
605
|
|
|
@@ -514,11 +616,11 @@ export const FUNCTION_NAME_BY_ALIAS: Readonly<Record<ModelAliasKey, string>> =
|
|
|
514
616
|
// ─── Provider Functions Index ────────────────────────────────────────────────
|
|
515
617
|
|
|
516
618
|
export type ProviderFunctionsIndex = Readonly<
|
|
517
|
-
Record<
|
|
619
|
+
Record<ModelCatalogProviderName, readonly ProviderFunctionEntry[]>
|
|
518
620
|
>;
|
|
519
621
|
|
|
520
622
|
export function buildProviderFunctionsIndex(): ProviderFunctionsIndex {
|
|
521
|
-
const index: Partial<Record<
|
|
623
|
+
const index: Partial<Record<ModelCatalogProviderName, ProviderFunctionEntry[]>> = {};
|
|
522
624
|
|
|
523
625
|
for (const alias of Object.keys(MODEL_CONFIG) as ModelAliasKey[]) {
|
|
524
626
|
const entry = (MODEL_CONFIG as Record<string, ModelConfigEntry>)[alias]!;
|
|
@@ -542,7 +644,7 @@ export function buildProviderFunctionsIndex(): ProviderFunctionsIndex {
|
|
|
542
644
|
}
|
|
543
645
|
|
|
544
646
|
// Freeze each provider array
|
|
545
|
-
for (const provider of Object.keys(index) as
|
|
647
|
+
for (const provider of Object.keys(index) as ModelCatalogProviderName[]) {
|
|
546
648
|
Object.freeze(index[provider]);
|
|
547
649
|
}
|
|
548
650
|
|
|
@@ -518,6 +518,58 @@ describe("runAgentStep", () => {
|
|
|
518
518
|
expect(logCalls[0]).toBe("writeLog:test-agent-agent-debug.log");
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
+
it("passes every observed event to a provided entry.onEvent", async () => {
|
|
522
|
+
const io = createFakeIO();
|
|
523
|
+
const events: HarnessEvent[] = [
|
|
524
|
+
{ harness: "claude", seq: 0, at: 1, raw: { text: "hello" }, type: "assistant_message", text: "hello" },
|
|
525
|
+
{ harness: "claude", seq: 1, at: 2, raw: { message: "done" }, type: "run_completed", result: makeSuccessResult() },
|
|
526
|
+
];
|
|
527
|
+
const run = mock((opts: { onEvent?: (event: HarnessEvent) => void }) => {
|
|
528
|
+
for (const event of events) {
|
|
529
|
+
opts.onEvent?.(event);
|
|
530
|
+
}
|
|
531
|
+
return makeFakeHarnessRun(makeSuccessResult());
|
|
532
|
+
});
|
|
533
|
+
const startMcpIoServer = mock(async () => createFakeMcpHandle());
|
|
534
|
+
|
|
535
|
+
const relayed: HarnessEvent[] = [];
|
|
536
|
+
await executeAgent(
|
|
537
|
+
{
|
|
538
|
+
io,
|
|
539
|
+
entry: { name: "a", harness: "claude", prompt: "p", onEvent: (e) => relayed.push(e) },
|
|
540
|
+
},
|
|
541
|
+
{ run, startMcpIoServer },
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
expect(relayed).toEqual(events);
|
|
545
|
+
const logCalls = io.calls.filter((c) => c.startsWith("writeLog:"));
|
|
546
|
+
expect(logCalls).toHaveLength(events.length);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("with no entry.onEvent, buffering and debug-log writes are unchanged", async () => {
|
|
550
|
+
const io = createFakeIO();
|
|
551
|
+
const events: HarnessEvent[] = [
|
|
552
|
+
{ harness: "claude", seq: 0, at: 1, raw: { text: "hello" }, type: "assistant_message", text: "hello" },
|
|
553
|
+
{ harness: "claude", seq: 1, at: 2, raw: { message: "done" }, type: "run_completed", result: makeSuccessResult() },
|
|
554
|
+
];
|
|
555
|
+
const run = mock((opts: { onEvent?: (event: HarnessEvent) => void }) => {
|
|
556
|
+
for (const event of events) {
|
|
557
|
+
opts.onEvent?.(event);
|
|
558
|
+
}
|
|
559
|
+
return makeFakeHarnessRun(makeSuccessResult());
|
|
560
|
+
});
|
|
561
|
+
const startMcpIoServer = mock(async () => createFakeMcpHandle());
|
|
562
|
+
|
|
563
|
+
const result = await executeAgent(
|
|
564
|
+
{ io, entry: { name: "a", harness: "claude", prompt: "p" } },
|
|
565
|
+
{ run, startMcpIoServer },
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(result.ok).toBe(true);
|
|
569
|
+
const logCalls = io.calls.filter((c) => c.startsWith("writeLog:"));
|
|
570
|
+
expect(logCalls).toHaveLength(events.length);
|
|
571
|
+
});
|
|
572
|
+
|
|
521
573
|
it("promptFrom reads the named artifact for the prompt", async () => {
|
|
522
574
|
let capturedIO: ReturnType<typeof createFakeIO> | undefined;
|
|
523
575
|
const createTaskFileIO = mock(() => {
|