@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.
Files changed (45) hide show
  1. package/docs/http-api.md +3 -0
  2. package/docs/pop-task-guide.md +8 -0
  3. package/package.json +5 -2
  4. package/src/cli/__tests__/analyze-task.test.ts +1 -2
  5. package/src/config/__tests__/config-schema.test.ts +42 -0
  6. package/src/config/__tests__/legacy-literal-guard.test.ts +156 -0
  7. package/src/config/__tests__/models.test.ts +139 -1
  8. package/src/config/models.ts +121 -19
  9. package/src/core/__tests__/agent-step.test.ts +52 -0
  10. package/src/core/__tests__/job-view.test.ts +98 -0
  11. package/src/core/agent-step.ts +5 -1
  12. package/src/core/agent-types.ts +6 -1
  13. package/src/core/job-view.ts +21 -1
  14. package/src/llm/__tests__/dispatch.test.ts +130 -0
  15. package/src/llm/__tests__/index.test.ts +17 -18
  16. package/src/llm/__tests__/legacy-normalization.test.ts +109 -0
  17. package/src/llm/index.ts +109 -112
  18. package/src/providers/__tests__/claude-code.test.ts +9 -3
  19. package/src/providers/__tests__/moonshot.test.ts +33 -3
  20. package/src/providers/__tests__/types.test.ts +43 -0
  21. package/src/providers/__tests__/zhipu.test.ts +24 -28
  22. package/src/providers/claude-code.ts +2 -2
  23. package/src/providers/moonshot.ts +7 -0
  24. package/src/providers/types.ts +7 -5
  25. package/src/providers/zhipu.ts +0 -2
  26. package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
  27. package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +36 -1
  28. package/src/task-analysis/analyzer.ts +4 -10
  29. package/src/task-analysis/enrichers/schema-deducer.ts +8 -9
  30. package/src/ui/dist/assets/{index-DN3-zvtP.js → index-lBbVeNZC.js} +5 -2
  31. package/src/ui/dist/assets/{index-DN3-zvtP.js.map → index-lBbVeNZC.js.map} +1 -1
  32. package/src/ui/dist/index.html +1 -1
  33. package/src/ui/pages/Code.tsx +5 -2
  34. package/src/ui/server/__tests__/http-api-contract.test.ts +838 -0
  35. package/src/ui/server/__tests__/job-control-endpoints.test.ts +134 -27
  36. package/src/ui/server/__tests__/job-control-service.test.ts +282 -0
  37. package/src/ui/server/__tests__/job-process-registry.test.ts +151 -0
  38. package/src/ui/server/__tests__/job-repository.test.ts +142 -0
  39. package/src/ui/server/config-bridge.ts +9 -6
  40. package/src/ui/server/embedded-assets.ts +4 -4
  41. package/src/ui/server/endpoints/gate-endpoints.ts +24 -12
  42. package/src/ui/server/endpoints/job-control-endpoints.ts +19 -763
  43. package/src/ui/server/job-control-service.ts +520 -0
  44. package/src/ui/server/job-process-registry.ts +250 -0
  45. package/src/ui/server/job-repository.ts +316 -0
package/src/llm/index.ts CHANGED
@@ -20,9 +20,14 @@ import { ensureMessagesPresent, isJsonResponseFormat } from "../providers/base.t
20
20
  import {
21
21
  MODEL_CONFIG,
22
22
  PROVIDER_FUNCTIONS,
23
+ PROVIDERS,
23
24
  getModelConfig,
25
+ normalizeLegacyProvider,
26
+ } from "../config/models.ts";
27
+ import type {
28
+ ProviderFunctionEntry,
29
+ ModelCatalogProviderName,
24
30
  } from "../config/models.ts";
25
- import type { ProviderFunctionsIndex } from "../config/models.ts";
26
31
  import type {
27
32
  ChatOptions,
28
33
  ChatResponse,
@@ -44,22 +49,6 @@ import type {
44
49
  } from "../providers/types.ts";
45
50
  import { ProviderJsonParseError } from "../providers/types.ts";
46
51
 
47
- // ─── Provider Name Mapping ───────────────────────────────────────────────────
48
- // types.ts uses "claudecode" (one word), config/models.ts uses "claude-code" (hyphenated).
49
-
50
- /** Maps gateway ProviderName to config provider key. */
51
- function toConfigProvider(name: ProviderName): string {
52
- if (name === "claudecode") return "claude-code";
53
- if (name === "zhipu") return "zai";
54
- return name;
55
- }
56
-
57
- /** Maps config provider key to gateway ProviderName. */
58
- function fromConfigProvider(configName: string): ProviderName {
59
- if (configName === "claude-code") return "claudecode";
60
- return configName as ProviderName;
61
- }
62
-
63
52
  // ─── Module State ────────────────────────────────────────────────────────────
64
53
 
65
54
  const llmEvents = new EventEmitter();
@@ -73,68 +62,82 @@ function warnUnknownAliasOnce(alias: string): void {
73
62
  console.warn(`[llm] unknown model alias "${alias}"; reporting cost as estimated`);
74
63
  }
75
64
 
65
+ function normalizeProviderOrThrow(name: string): ProviderName {
66
+ const canonical = normalizeLegacyProvider(name);
67
+ if (!canonical) {
68
+ throw new Error(`Unknown provider: ${name}`);
69
+ }
70
+ return canonical;
71
+ }
72
+
76
73
  // ─── Adapter Dispatch ────────────────────────────────────────────────────────
77
74
 
78
- async function dispatchByProvider(
79
- options: ChatOptions,
80
- ): Promise<AdapterResponse> {
81
- const { provider, messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs } = options;
82
-
83
- switch (provider) {
84
- case "alibaba":
85
- return alibabaChat({
86
- messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs,
87
- frequencyPenalty: options.frequencyPenalty,
88
- presencePenalty: options.presencePenalty,
89
- });
90
- case "anthropic":
91
- return anthropicChat({ messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs });
92
- case "openai":
93
- return openaiChat({
94
- messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs,
95
- seed: undefined,
96
- frequencyPenalty: options.frequencyPenalty,
97
- presencePenalty: options.presencePenalty,
98
- });
99
- case "gemini":
100
- return geminiChat({
101
- messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs,
102
- frequencyPenalty: options.frequencyPenalty,
103
- presencePenalty: options.presencePenalty,
104
- });
105
- case "deepseek":
106
- return deepseekChat({
107
- messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs,
108
- frequencyPenalty: options.frequencyPenalty,
109
- presencePenalty: options.presencePenalty,
110
- });
111
- case "moonshot":
112
- return moonshotChat({ messages, model, maxTokens, responseFormat, maxRetries, requestTimeoutMs });
113
- case "zai":
114
- case "zhipu":
115
- return zaiChat({ messages, model, temperature, maxTokens, responseFormat, topP, stop, maxRetries, requestTimeoutMs });
116
- case "claudecode":
117
- return claudeCodeChat({ messages, model, maxTokens, responseFormat, maxRetries, requestTimeoutMs });
118
- case "opencode":
119
- return opencodeChat({
120
- messages, model, responseFormat, maxRetries, requestTimeoutMs,
121
- opencode: options.opencode,
122
- });
123
- case "mock": {
124
- if (!mockProvider) {
125
- throw new Error("No mock provider registered. Call registerMockProvider() first.");
126
- }
127
- return mockProvider.chat(options);
75
+ type AdapterFn = (opts: ChatOptions) => Promise<AdapterResponse>;
76
+
77
+ const ADAPTERS: Record<ProviderName, AdapterFn> = {
78
+ openai: (opts) => openaiChat(opts),
79
+ anthropic: (opts) => anthropicChat(opts),
80
+ gemini: (opts) => geminiChat(opts),
81
+ deepseek: (opts) => deepseekChat(opts),
82
+ moonshot: (opts) => moonshotChat(opts),
83
+ zai: (opts) => zaiChat(opts),
84
+ alibaba: (opts) => alibabaChat(opts),
85
+ "claude-code": (opts) => claudeCodeChat(opts),
86
+ opencode: (opts) => opencodeChat(opts),
87
+ mock: (opts) => {
88
+ if (!mockProvider) {
89
+ throw new Error("No mock provider registered. Call registerMockProvider() first.");
128
90
  }
129
- default:
130
- throw new Error(`Unknown provider: ${provider as string}`);
91
+ return mockProvider.chat(opts);
92
+ },
93
+ };
94
+
95
+ const CLI_AVAILABILITY: Partial<Record<ProviderName, () => boolean>> = {
96
+ "claude-code": isClaudeCodeAvailable,
97
+ opencode: isOpenCodeAvailable,
98
+ };
99
+
100
+ function buildAdapterOptions(options: ChatOptions): ChatOptions {
101
+ const declared = PROVIDERS[options.provider].sampling;
102
+ const opts: ChatOptions = {
103
+ provider: options.provider,
104
+ messages: options.messages,
105
+ model: options.model,
106
+ maxTokens: options.maxTokens,
107
+ responseFormat: options.responseFormat,
108
+ maxRetries: options.maxRetries,
109
+ requestTimeoutMs: options.requestTimeoutMs,
110
+ };
111
+ if (options.opencode !== undefined) {
112
+ opts.opencode = options.opencode;
113
+ }
114
+ if (declared.includes("temperature") && options.temperature !== undefined) {
115
+ opts.temperature = options.temperature;
116
+ }
117
+ if (declared.includes("topP") && options.topP !== undefined) {
118
+ opts.topP = options.topP;
119
+ }
120
+ if (declared.includes("stop") && options.stop !== undefined) {
121
+ opts.stop = options.stop;
131
122
  }
123
+ if (declared.includes("frequencyPenalty") && options.frequencyPenalty !== undefined) {
124
+ opts.frequencyPenalty = options.frequencyPenalty;
125
+ }
126
+ if (declared.includes("presencePenalty") && options.presencePenalty !== undefined) {
127
+ opts.presencePenalty = options.presencePenalty;
128
+ }
129
+ return opts;
132
130
  }
133
131
 
134
132
  async function callAdapter(
135
133
  options: ChatOptions,
136
134
  ): Promise<AdapterResponse> {
137
- const response = await dispatchByProvider(options);
135
+ const adapter = ADAPTERS[options.provider] as AdapterFn | undefined;
136
+ if (!adapter) {
137
+ throw new Error(`Unknown provider: ${options.provider as string}`);
138
+ }
139
+ const adapterOptions = buildAdapterOptions(options);
140
+ const response = await adapter(adapterOptions);
138
141
  response.usageAvailable = resolveUsageAvailable(options.provider, options);
139
142
  return response;
140
143
  }
@@ -173,14 +176,14 @@ async function performJsonRepair(
173
176
  /**
174
177
  * Returns whether a provider can structurally report token usage for the given
175
178
  * options. This is the single source of truth for `usageAvailable`; adapters
176
- * never set it themselves. Forward-compatible with the #25 provider registry,
177
- * where this becomes a registry column.
179
+ * never set it themselves. The base capability comes from
180
+ * `PROVIDERS[provider].usageAvailable`; OpenCode keeps its runtime mode/baseUrl
181
+ * exception because SDK mode reports usage while CLI mode does not.
178
182
  */
179
183
  export function resolveUsageAvailable(
180
184
  provider: ProviderName,
181
185
  options: ChatOptions,
182
186
  ): boolean {
183
- if (provider === "claudecode") return false;
184
187
  if (provider === "opencode") {
185
188
  if (options.opencode?.mode === "cli") return false;
186
189
  if (options.opencode?.mode === "sdk") return true;
@@ -190,7 +193,7 @@ export function resolveUsageAvailable(
190
193
  process.env.OPENCODE_BASE_URL;
191
194
  return baseUrl != null;
192
195
  }
193
- return true;
196
+ return PROVIDERS[provider].usageAvailable;
194
197
  }
195
198
 
196
199
  // ─── Usage Normalization ─────────────────────────────────────────────────────
@@ -352,6 +355,8 @@ async function writeDebugLog(options: ChatOptions, response: ChatResponse): Prom
352
355
  // ─── Public API ──────────────────────────────────────────────────────────────
353
356
 
354
357
  export async function chat(options: ChatOptions): Promise<ChatResponse> {
358
+ const provider = normalizeProviderOrThrow(options.provider);
359
+ options = { ...options, provider };
355
360
  ensureMessagesPresent(options.messages, options.provider);
356
361
  const configTimeout = getConfig().taskRunner.llmRequestTimeout;
357
362
  const opts = {
@@ -415,7 +420,7 @@ export async function chat(options: ChatOptions): Promise<ChatResponse> {
415
420
  : 0;
416
421
  const costEstimated = price === null || usage.source !== "reported";
417
422
  if (price === null) {
418
- warnUnknownAliasOnce(`${toConfigProvider(opts.provider)}:${model}`);
423
+ warnUnknownAliasOnce(`${opts.provider}:${model}`);
419
424
  }
420
425
  const duration = Date.now() - startTime;
421
426
 
@@ -474,7 +479,7 @@ export async function complete(
474
479
  prompt: string,
475
480
  options?: Partial<ChatOptions>,
476
481
  ): Promise<ChatResponse> {
477
- const defaultProvider = getConfig().llm.defaultProvider as ProviderName;
482
+ const defaultProvider = normalizeProviderOrThrow(getConfig().llm.defaultProvider);
478
483
  return chat({
479
484
  provider: defaultProvider,
480
485
  messages: [{ role: "user", content: prompt }],
@@ -512,6 +517,7 @@ export function createHighLevelLLM(options?: Partial<ChatOptions>): HighLevelLLM
512
517
  export function createLLMWithOverride(
513
518
  override: { provider: ProviderName; model: string },
514
519
  ): HighLevelLLM {
520
+ const canonicalProvider = normalizeProviderOrThrow(override.provider);
515
521
  const base = createHighLevelLLM();
516
522
 
517
523
  const GUARDED_METHODS = new Set([
@@ -538,11 +544,11 @@ export function createLLMWithOverride(
538
544
  // If accessing known top-level methods, return them bound with override
539
545
  if (prop === "chat") {
540
546
  return (opts: ChatOptions) =>
541
- chat({ ...opts, provider: override.provider, model: override.model });
547
+ chat({ ...opts, provider: canonicalProvider, model: override.model });
542
548
  }
543
549
  if (prop === "complete") {
544
550
  return (prompt: string, opts?: Partial<ChatOptions>) =>
545
- complete(prompt, { ...opts, provider: override.provider, model: override.model });
551
+ complete(prompt, { ...opts, provider: canonicalProvider, model: override.model });
546
552
  }
547
553
  if (prop === "createChain" || prop === "withRetry" || prop === "parallel" || prop === "getAvailableProviders") {
548
554
  return Reflect.get(target, prop, receiver);
@@ -560,7 +566,7 @@ export function createLLMWithOverride(
560
566
  return (opts?: Partial<ChatOptions>) =>
561
567
  chat({
562
568
  ...opts,
563
- provider: override.provider,
569
+ provider: canonicalProvider,
564
570
  model: override.model,
565
571
  } as ChatOptions);
566
572
  },
@@ -571,7 +577,7 @@ export function createLLMWithOverride(
571
577
  return (opts?: Partial<ChatOptions>) =>
572
578
  chat({
573
579
  ...opts,
574
- provider: override.provider,
580
+ provider: canonicalProvider,
575
581
  model: override.model,
576
582
  } as ChatOptions);
577
583
  },
@@ -679,19 +685,18 @@ export function registerMockProvider(provider: MockProvider): void {
679
685
  }
680
686
 
681
687
  export function getAvailableProviders(): ProviderAvailability {
682
- return {
683
- alibaba: !!process.env["ALIBABA_API_KEY"],
684
- openai: !!process.env["OPENAI_API_KEY"],
685
- anthropic: !!process.env["ANTHROPIC_API_KEY"],
686
- gemini: !!process.env["GEMINI_API_KEY"],
687
- deepseek: !!process.env["DEEPSEEK_API_KEY"],
688
- zai: !!(process.env["ZAI_API_KEY"] ?? process.env["ZHIPU_API_KEY"]),
689
- zhipu: !!(process.env["ZAI_API_KEY"] ?? process.env["ZHIPU_API_KEY"]),
690
- moonshot: !!process.env["MOONSHOT_API_KEY"],
691
- claudecode: isClaudeCodeAvailable(),
692
- mock: mockProvider !== null,
693
- opencode: isOpenCodeAvailable(),
694
- };
688
+ const availability = {} as ProviderAvailability;
689
+ for (const name of Object.keys(PROVIDERS) as ProviderName[]) {
690
+ if (name === "mock") {
691
+ availability[name] = mockProvider !== null;
692
+ continue;
693
+ }
694
+ const meta = PROVIDERS[name];
695
+ const envAvailable = meta.envKeys.some((key) => !!process.env[key]);
696
+ const cliAvailable = CLI_AVAILABILITY[name]?.() ?? false;
697
+ availability[name] = envAvailable || cliAvailable;
698
+ }
699
+ return availability;
695
700
  }
696
701
 
697
702
  export function estimateTokens(text: string): number {
@@ -704,7 +709,7 @@ export interface ModelPrice {
704
709
  }
705
710
 
706
711
  /**
707
- * Looks up the per-million-token price for a gateway `(provider, model)` pair.
712
+ * Looks up the per-million-token price for a `(provider, model)` pair.
708
713
  * Returns `null` when no price is known. This is the single cost-lookup surface
709
714
  * used by both `calculateCost` and the `costEstimated` discriminator in `chat()`.
710
715
  */
@@ -712,8 +717,9 @@ export function findModelPrice(
712
717
  provider: string,
713
718
  model: string,
714
719
  ): ModelPrice | null {
715
- const configProvider = toConfigProvider(provider as ProviderName);
716
- const alias = `${configProvider}:${model}`;
720
+ const canonical = normalizeLegacyProvider(provider);
721
+ if (!canonical) return null;
722
+ const alias = `${canonical}:${model}`;
717
723
  const direct = getModelConfig(alias);
718
724
  if (direct) {
719
725
  return {
@@ -722,7 +728,7 @@ export function findModelPrice(
722
728
  };
723
729
  }
724
730
  for (const entry of Object.values(MODEL_CONFIG)) {
725
- if (entry.provider === configProvider && entry.model === model) {
731
+ if (entry.provider === canonical && entry.model === model) {
726
732
  return {
727
733
  tokenCostInPerMillion: entry.tokenCostInPerMillion,
728
734
  tokenCostOutPerMillion: entry.tokenCostOutPerMillion,
@@ -749,31 +755,22 @@ export function calculateCost(
749
755
  function buildProviderModelMap(): ProviderModelMap {
750
756
  const map: ProviderModelMap = {};
751
757
 
752
- for (const [configProviderName, entries] of Object.entries(
753
- PROVIDER_FUNCTIONS as ProviderFunctionsIndex,
754
- )) {
755
- const gatewayProvider = fromConfigProvider(configProviderName);
758
+ for (const [providerName, entries] of Object.entries(PROVIDER_FUNCTIONS) as [
759
+ ModelCatalogProviderName,
760
+ ProviderFunctionEntry[],
761
+ ][]) {
756
762
  const group: ProviderGroup = {};
757
763
 
758
764
  for (const entry of entries) {
759
765
  group[entry.functionName] = (opts?: Partial<ChatOptions>) =>
760
766
  chat({
761
- provider: gatewayProvider,
767
+ provider: providerName,
762
768
  model: entry.model,
763
769
  ...opts,
764
770
  } as ChatOptions);
765
771
  }
766
772
 
767
- map[gatewayProvider] = group;
768
-
769
- // Preserve config-key access for callers still using hyphenated config names.
770
- if (configProviderName !== gatewayProvider) {
771
- map[configProviderName] = group;
772
- }
773
-
774
- if (gatewayProvider === "zai") {
775
- map["zhipu"] = group;
776
- }
773
+ map[providerName] = group;
777
774
  }
778
775
 
779
776
  return map;
@@ -125,9 +125,15 @@ describe("claudeCodeChat", () => {
125
125
  const plainText = "This is not JSON";
126
126
  spawnMock.mockReturnValue(createMockProcFromText(plainText));
127
127
 
128
- await expect(claudeCodeChat(baseOptions)).rejects.toBeInstanceOf(
129
- ProviderJsonParseError,
130
- );
128
+ try {
129
+ await claudeCodeChat(baseOptions);
130
+ expect.unreachable("should have thrown");
131
+ } catch (err) {
132
+ expect(err).toBeInstanceOf(ProviderJsonParseError);
133
+ const parseErr = err as ProviderJsonParseError;
134
+ expect(parseErr.provider).toBe("claude-code");
135
+ expect(parseErr.model).toBe("sonnet");
136
+ }
131
137
  });
132
138
 
133
139
  it("combines system and user messages into the prompt", async () => {
@@ -175,7 +175,7 @@ describe("moonshotChat", () => {
175
175
  expect(headers["Content-Type"]).toBe("application/json");
176
176
  });
177
177
 
178
- it("does not include temperature, topP, frequencyPenalty, or presencePenalty in request body", async () => {
178
+ it("omits temperature, top_p, and stop from request body when not supplied", async () => {
179
179
  const events = makeOpenAiSseEvents([JSON.stringify({ ok: true })]);
180
180
  fetchMock.mockResolvedValue(mockStreamingResponse(events));
181
181
 
@@ -186,8 +186,38 @@ describe("moonshotChat", () => {
186
186
  );
187
187
  expect(body.temperature).toBeUndefined();
188
188
  expect(body.top_p).toBeUndefined();
189
- expect(body.frequency_penalty).toBeUndefined();
190
- expect(body.presence_penalty).toBeUndefined();
189
+ expect(body.stop).toBeUndefined();
190
+ });
191
+
192
+ it("includes temperature, top_p, and stop in request body when supplied", async () => {
193
+ const events = makeOpenAiSseEvents([JSON.stringify({ ok: true })]);
194
+ fetchMock.mockResolvedValue(mockStreamingResponse(events));
195
+
196
+ await moonshotChat({
197
+ ...baseOptions,
198
+ temperature: 0.4,
199
+ topP: 0.8,
200
+ stop: ["\n\nUser:"],
201
+ });
202
+
203
+ const body = JSON.parse(
204
+ (fetchMock.mock.calls[0] as [string, RequestInit])[1].body as string,
205
+ );
206
+ expect(body.temperature).toBe(0.4);
207
+ expect(body.top_p).toBe(0.8);
208
+ expect(body.stop).toEqual(["\n\nUser:"]);
209
+ });
210
+
211
+ it("passes stop as a string when given a string", async () => {
212
+ const events = makeOpenAiSseEvents([JSON.stringify({ ok: true })]);
213
+ fetchMock.mockResolvedValue(mockStreamingResponse(events));
214
+
215
+ await moonshotChat({ ...baseOptions, stop: "END" });
216
+
217
+ const body = JSON.parse(
218
+ (fetchMock.mock.calls[0] as [string, RequestInit])[1].body as string,
219
+ );
220
+ expect(body.stop).toBe("END");
191
221
  });
192
222
 
193
223
  describe("content-filter fallback to DeepSeek", () => {
@@ -5,6 +5,7 @@ import {
5
5
  type ChatOptions,
6
6
  type LLMRequestCompleteEvent,
7
7
  type LLMRequestErrorEvent,
8
+ type MoonshotOptions,
8
9
  type NormalizedUsage,
9
10
  type OpenCodeOptions,
10
11
  type OpenCodePermissionAction,
@@ -13,6 +14,8 @@ import {
13
14
  type OpenCodePermissionName,
14
15
  type OpenCodePermissionRule,
15
16
  type OpenCodeRequestConfig,
17
+ type ProviderAvailability,
18
+ type ProviderName,
16
19
  type UsageSource,
17
20
  } from "../types.ts";
18
21
 
@@ -192,6 +195,46 @@ describe("OpenCode types", () => {
192
195
  });
193
196
  });
194
197
 
198
+ describe("provider type contracts", () => {
199
+ it("ProviderName excludes the legacy zhipu and claudecode spellings", () => {
200
+ type HasLegacy = "zhipu" | "claudecode" extends ProviderName ? true : false;
201
+ const legacyExcluded: HasLegacy = false;
202
+ expect(legacyExcluded).toBe(false);
203
+ });
204
+
205
+ it("ProviderName includes canonical claude-code and zai", () => {
206
+ type HasClaudeCode = "claude-code" extends ProviderName ? true : false;
207
+ type HasZai = "zai" extends ProviderName ? true : false;
208
+ const claudeCodePresent: HasClaudeCode = true;
209
+ const zaiPresent: HasZai = true;
210
+ expect(claudeCodePresent).toBe(true);
211
+ expect(zaiPresent).toBe(true);
212
+ });
213
+
214
+ it("ProviderAvailability is keyed by canonical claude-code, not legacy spellings", () => {
215
+ type HasClaudeCode =
216
+ "claude-code" extends keyof ProviderAvailability ? true : false;
217
+ type HasLegacyKeys =
218
+ "zhipu" | "claudecode" extends keyof ProviderAvailability ? true : false;
219
+ const claudeCodePresent: HasClaudeCode = true;
220
+ const legacyAbsent: HasLegacyKeys = false;
221
+ expect(claudeCodePresent).toBe(true);
222
+ expect(legacyAbsent).toBe(false);
223
+ });
224
+
225
+ it("MoonshotOptions accepts temperature, topP, and stop sampling params", () => {
226
+ const options: MoonshotOptions = {
227
+ messages: [{ role: "user", content: "hello" }],
228
+ temperature: 0.5,
229
+ topP: 0.9,
230
+ stop: ["\n\n"],
231
+ };
232
+ expect(options.temperature).toBe(0.5);
233
+ expect(options.topP).toBe(0.9);
234
+ expect(options.stop).toEqual(["\n\n"]);
235
+ });
236
+ });
237
+
195
238
  describe("usage telemetry contracts", () => {
196
239
  it("NormalizedUsage requires a source field", () => {
197
240
  const usage: NormalizedUsage = {