@hypit/hypit 0.1.7 → 0.1.9

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 (46) hide show
  1. package/README.md +15 -4
  2. package/dist/public/endpoint-kit.d.ts +1 -1
  3. package/dist/public/runtime-kit.d.ts +3 -1
  4. package/examples/provider-package/README.md +12 -0
  5. package/examples/provider-package/packages/provider-images/src/provider.ts +37 -8
  6. package/package.json +1 -1
  7. package/packages/cli/README.md +35 -0
  8. package/packages/cli/src/arguments.ts +12 -1
  9. package/packages/cli/src/command-hint.ts +20 -0
  10. package/packages/cli/src/commands/environment.ts +39 -18
  11. package/packages/cli/src/commands/execution.ts +34 -18
  12. package/packages/cli/src/commands/results.ts +8 -3
  13. package/packages/cli/src/machine-view.ts +4 -2
  14. package/packages/cli/src/main.ts +25 -23
  15. package/packages/cli/src/observation.ts +7 -2
  16. package/packages/cli/src/output.ts +8 -0
  17. package/packages/cli/src/usage-error.ts +8 -0
  18. package/packages/cli/src/view.ts +4 -1
  19. package/packages/driver-node/README.md +5 -0
  20. package/packages/driver-node/src/driver.ts +20 -15
  21. package/packages/endpoint-kit/README.md +11 -2
  22. package/packages/endpoint-kit/src/index.ts +1 -1
  23. package/packages/generation/README.md +10 -0
  24. package/packages/provider-hypihub/README.md +41 -6
  25. package/packages/provider-hypihub/src/errors.ts +61 -0
  26. package/packages/provider-hypihub/src/mapping.ts +10 -25
  27. package/packages/provider-hypihub/src/oauth.ts +4 -1
  28. package/packages/provider-hypihub/src/provider.ts +79 -74
  29. package/packages/provider-hypihub/src/routes.ts +24 -3
  30. package/packages/provider-hypihub/src/upload.ts +8 -18
  31. package/packages/provider-whisperx-local/README.md +7 -1
  32. package/packages/provider-whisperx-local/src/program.ts +2 -0
  33. package/packages/runtime-host-node/src/index.ts +4 -0
  34. package/packages/runtime-kit/README.md +3 -0
  35. package/packages/runtime-kit/src/index.ts +2 -0
  36. package/packages/runtime-local/README.md +17 -0
  37. package/packages/runtime-local/package.json +2 -1
  38. package/packages/runtime-local/src/program-lock.ts +40 -0
  39. package/packages/runtime-local/src/programs.ts +156 -79
  40. package/packages/video-cli/README.md +33 -6
  41. package/packages/video-cli/src/cli.ts +4 -1
  42. package/packages/video-cli/src/creation.ts +7 -2
  43. package/packages/video-cli/src/index.ts +2 -0
  44. package/packages/video-cli/src/version.ts +90 -0
  45. package/services/whisperx/README.md +10 -3
  46. package/services/whisperx/src/hypit_whisperx_service/engine.py +19 -1
@@ -9,6 +9,8 @@ type ProgramStateView = {
9
9
  readonly detail?: string;
10
10
  readonly pid?: number;
11
11
  readonly logPath?: string;
12
+ readonly installationLogPath?: string;
13
+ readonly errorLogPath?: string;
12
14
  };
13
15
  type AttentionView = { readonly message: string; readonly action?: string };
14
16
 
@@ -18,12 +20,12 @@ export type OperationalMachineView =
18
20
  | { readonly format: "hypit.cli-runtime-selection@1"; readonly selected: boolean; readonly removed?: boolean; readonly profile?: string; readonly project?: string }
19
21
  | { readonly format: "hypit.cli-paths@1"; readonly project: string; readonly projectState: string; readonly profileSource: "argument" | "project" | "none"; readonly selectionFile: string; readonly profile?: string; readonly runtimeData?: string; readonly hostState: string; readonly machinePackages: string; readonly distribution?: string }
20
22
  | { readonly format: "hypit.cli-package@1"; readonly action: "install" | "status"; readonly package: string; readonly ready: boolean }
21
- | { readonly format: "hypit.cli-programs@1"; readonly action: "up" | "down" | "status"; readonly ready: boolean; readonly programCount: number; readonly readyCount: number; readonly programs: readonly ProgramStateView[]; readonly omittedPrograms?: number }
23
+ | { readonly format: "hypit.cli-programs@1"; readonly action: "up" | "down" | "status"; readonly ok: boolean; readonly ready: boolean; readonly programCount: number; readonly readyCount: number; readonly programs: readonly ProgramStateView[]; readonly omittedPrograms?: number }
22
24
  | { readonly format: "hypit.cli-runtime-up@1"; readonly ready: boolean; readonly worker: string; readonly preparedPackages: number; readonly programs: { readonly total: number; readonly ready: number; readonly items: readonly ProgramStateView[] } }
23
25
  | { readonly format: "hypit.cli-runtime-logs@1"; readonly lines: readonly string[]; readonly totalLines: number; readonly omittedLines: number; readonly path?: string }
24
26
  | { readonly format: "hypit.cli-runtime-down@1"; readonly worker: string }
25
27
  | { readonly format: "hypit.cli-runtime-status@1"; readonly ready: boolean; readonly attention: boolean; readonly worker: { readonly state: string; readonly configuration?: string }; readonly builds: Readonly<Record<string, number>>; readonly programs: { readonly total: number; readonly ready: number; readonly unavailable: readonly ProgramStateView[] }; readonly capacity: { readonly active: number } }
26
- | { readonly format: "hypit.cli-auth-status@1"; readonly endpoint: string; readonly credentials: readonly { readonly endpoint: string; readonly slot: string; readonly label: string; readonly kind: string; readonly configured: boolean; readonly writable: boolean }[]; readonly omittedCredentials?: number }
28
+ | { readonly format: "hypit.cli-auth-status@1"; readonly endpoint: string; readonly credentials: readonly { readonly endpoint: string; readonly slot: string; readonly label: string; readonly kind: string; readonly configured: boolean; readonly writable: boolean; readonly acquisition?: { readonly kind: string; readonly authorizationEndpoint: string } }[]; readonly omittedCredentials?: number }
27
29
  | { readonly format: "hypit.cli-auth-change@1"; readonly endpoint: string; readonly slot: string; readonly configured: boolean; readonly changed?: boolean }
28
30
  | { readonly format: "hypit.cli-builds@1"; readonly builds: readonly { readonly id: string; readonly createdAt: string; readonly title?: string; readonly outcome: string; readonly run?: string; readonly targetCount: number; readonly targets?: readonly string[]; readonly omittedTargets?: number; readonly outputCount: number }[]; readonly next?: string }
29
31
  | { readonly format: "hypit.cli-history@1"; readonly output: string; readonly source?: string; readonly entries: readonly { readonly build: string; readonly title?: string; readonly createdAt: string; readonly outcome: string; readonly output: CliOutputView }[]; readonly next?: string }
@@ -17,6 +17,7 @@ import { createPlanOutput, createPricingOutput, writeCliHelp, writeCliOutput } f
17
17
  import type { CliIo, CliMachineView } from "./output.js";
18
18
  import { parseCommand } from "./arguments.js";
19
19
  import type { CliCommand, RuntimeOption } from "./command.js";
20
+ import { commandHint } from "./command-hint.js";
20
21
  import { assertPlannedRequests, assertPreflight, createCatalogDescriptor, describePlanNeeds, describePlanPricing, describePlanProviders, evaluatePlanNeeds, preflightPlan } from "./build-planning.js";
21
22
  import { buildProgressLines, observeBuild } from "./observation.js";
22
23
  import { isProjectResultCommand, runProjectResultCommand } from "./commands/results.js";
@@ -62,7 +63,8 @@ export async function runCli(
62
63
  distribution: CliDistribution,
63
64
  ): Promise<void> {
64
65
  if (argv.length === 0 || argv[0] === "help" || argv.includes("--help")) {
65
- const topic = argv[0] === "help" ? argv[1] : argv.includes("--help") ? argv[0] : undefined;
66
+ const topic = argv[0] === "help" ? argv[1] : argv[0] === "--help" ? undefined
67
+ : argv.includes("--help") ? argv[0] : undefined;
66
68
  writeCliHelp(io, topic);
67
69
  return;
68
70
  }
@@ -179,14 +181,11 @@ export async function runCli(
179
181
 
180
182
  let runtimeProfile = acceptsRuntimeContext(args) ? args.runtimeProfile : undefined;
181
183
  let runtimeSelectionFile: string | undefined;
182
- const runtimeWasExplicit = runtimeProfile !== undefined;
183
- let runtimeNeedsHint = runtimeWasExplicit;
184
184
  if (acceptsRuntimeContext(args) && runtimeProfile === undefined && args.command !== "logs") {
185
185
  const selected = await findRuntimeProfile(await commandProjectRoot());
186
186
  if (selected !== undefined) {
187
187
  runtimeProfile = selected.profile;
188
188
  runtimeSelectionFile = selected.selectionFile;
189
- runtimeNeedsHint = false;
190
189
  }
191
190
  }
192
191
  const runtimeController = async (profile: string): Promise<CliRuntimeController> => {
@@ -227,6 +226,7 @@ export async function runCli(
227
226
  if (isExecutionCommand(args)) {
228
227
  await runExecutionCommand({
229
228
  args,
229
+ projectRoot: await commandProjectRoot(),
230
230
  runtimeProfile,
231
231
  io,
232
232
  runtimeHost,
@@ -346,6 +346,7 @@ export async function runCli(
346
346
  if (runtimeProfile === undefined) {
347
347
  throw new Error("build requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
348
348
  }
349
+ const commandScope = { projectRoot: projectResultsRoot, runtimeProfile: resolve(runtimeProfile) };
349
350
  const buildResults = await projectResults(projectResultsRoot);
350
351
  let loadedRun;
351
352
  try {
@@ -434,7 +435,7 @@ export async function runCli(
434
435
  ]),
435
436
  ].join(" · ");
436
437
  if (args.follow && "view" in built && !args.presentation.json) {
437
- const acceptedView = buildStatusView({ id: built.id, runtime: built.view });
438
+ const acceptedView = buildStatusView({ id: built.id, runtime: built.view, commandScope });
438
439
  const targets = built.view.targets.slice(0, args.limit);
439
440
  writeOperational({
440
441
  format: "hypit.cli-build@1",
@@ -451,15 +452,15 @@ export async function runCli(
451
452
  built = await observeBuild(runtime, built, {
452
453
  ...(args.maxWaitMs === undefined ? {} : { maxWaitMs: args.maxWaitMs }),
453
454
  controller,
455
+ commandScope,
454
456
  readResult: async () => await buildResults.repository.read(built.id),
455
- ...(args.presentation.json ? {} : {
456
- onProgress: (progress) => {
457
- for (const line of buildProgressLines(progress, {
458
- verbose: args.presentation.verbose,
459
- limit: args.limit,
460
- })) io.write(`${line}\n`);
461
- },
462
- }),
457
+ onProgress: (progress) => {
458
+ const report = io.writeProgress ?? (args.presentation.json ? undefined : io.write);
459
+ for (const line of buildProgressLines(progress, {
460
+ verbose: args.presentation.verbose,
461
+ limit: args.limit,
462
+ })) report?.(`${line}\n`);
463
+ },
463
464
  });
464
465
  }
465
466
  const finished = "completion" in built;
@@ -490,6 +491,7 @@ export async function runCli(
490
491
  });
491
492
  const buildView = buildStatusView({
492
493
  id: built.id,
494
+ commandScope,
493
495
  ...(activeView === undefined ? {} : { runtime: activeView }),
494
496
  ...(finishedResult === undefined ? {} : { result: finishedResult }),
495
497
  verbose: args.presentation.verbose,
@@ -500,7 +502,7 @@ export async function runCli(
500
502
  ? buildView
501
503
  : { ...buildView, title: args.title },
502
504
  };
503
- const runtimeHint = runtimeNeedsHint ? ` --runtime ${runtimeProfile}` : "";
505
+ const resultScope = { projectRoot: projectResultsRoot };
504
506
  const resultTargets = finishedResult?.targets.flatMap((name) => {
505
507
  const output = finishedResult.outputs[name];
506
508
  return output === undefined ? [] : [{ name, output }];
@@ -508,11 +510,11 @@ export async function runCli(
508
510
  const finishedLines = resultTargets.length === 0 && targetPresentations.length === 0
509
511
  ? [
510
512
  ...(completionReason === undefined ? [] : [`Reason ${completionReason}`]),
511
- `Inspect hypit inspect ${built.id}`,
513
+ `Inspect ${commandHint(["inspect", built.id], resultScope)}`,
512
514
  ]
513
515
  : [
514
516
  ...(completionReason === undefined ? [] : [`Reason ${completionReason}`]),
515
- `Inspect hypit inspect ${built.id}`,
517
+ `Inspect ${commandHint(["inspect", built.id], resultScope)}`,
516
518
  ...resultTargets
517
519
  .filter((item) => item.output.value.kind === "inline")
518
520
  .slice(0, args.limit)
@@ -523,14 +525,14 @@ export async function runCli(
523
525
  .filter((item) => item.output.value.kind !== "inline")
524
526
  .slice(0, args.limit)
525
527
  .map((item) =>
526
- `Export hypit get ${built.id} --output ${item.name} --to <path>`),
528
+ `Export ${commandHint(["get", built.id, "--output", item.name], resultScope)} --to <path>`),
527
529
  ...(resultTargets.length > 0 ? [] : targetPresentations
528
530
  .filter((item) => item.inline !== undefined)
529
531
  .slice(0, args.limit)
530
532
  .map((item) => `Result ${item.published.name} = ${item.inline}`)),
531
533
  ];
532
534
  const humanTitle = issue !== undefined
533
- ? "Result needs attention"
535
+ ? "Build needs attention"
534
536
  : finished
535
537
  ? buildOutcome === "complete"
536
538
  ? "Build complete"
@@ -550,14 +552,14 @@ export async function runCli(
550
552
  ] : []),
551
553
  ...(issue === undefined ? [] : [
552
554
  ["Execution", buildOutcome ?? machine.build.work.state] as const,
553
- ["Result", "needs attention"] as const,
555
+ [issue.scope === "cleanup" ? "Cleanup" : "Result", "needs attention"] as const,
554
556
  ]),
555
557
  ], finished ? finishedLines : issue !== undefined ? [
556
- `Result ${issue.scope}: ${issue.message}`,
557
- `Finish hypit result finish ${built.id}${runtimeHint}`,
558
+ `Attention ${issue.scope}: ${issue.message}`,
559
+ `Finish ${commandHint(["result", "finish", built.id], commandScope)}`,
558
560
  ] : [
559
- `Watch hypit status ${built.id}${runtimeHint} --watch`,
560
- `Cancel hypit cancel ${built.id}${runtimeHint}`,
561
+ `Watch ${commandHint(["status", built.id, "--watch"], commandScope)}`,
562
+ `Cancel ${commandHint(["cancel", built.id], commandScope)}`,
561
563
  ]);
562
564
  if (buildOutcome === "failed" || issue !== undefined) io.setExitCode?.(1);
563
565
  } finally {
@@ -1,6 +1,8 @@
1
1
  import type { BuildResultManifest } from "@hypit/build-result";
2
2
 
3
3
  import type { CliBuildSubmission, CliBuildView, CliRuntimeControl, CliRuntimeController } from "./runtime-port.js";
4
+ import { commandHint } from "./command-hint.js";
5
+ import type { CommandScope } from "./command-hint.js";
4
6
  import { formatOperationProgress } from "./runtime-view.js";
5
7
 
6
8
  const FIRST_HEARTBEAT_MS = 30_000;
@@ -122,6 +124,7 @@ export async function observeBuildView(
122
124
  readonly maxWaitMs?: number;
123
125
  readonly controller?: CliRuntimeController;
124
126
  readonly onProgress?: (value: BuildProgressView) => void;
127
+ readonly commandScope?: CommandScope;
125
128
  } = {},
126
129
  ): Promise<CliBuildView | undefined> {
127
130
  let view: CliBuildView | undefined = initial;
@@ -158,8 +161,9 @@ export async function observeBuildView(
158
161
  const worker = await options.controller.worker.status();
159
162
  if (worker.state !== "running") {
160
163
  throw new Error(
161
- `Runtime Worker is ${worker.state}; Build ${build} remains durable. `
162
- + `Run hypit runtime up, then run hypit status ${build} --watch again`,
164
+ `Runtime Worker is ${worker.state}; stopped watching Build ${build}. `
165
+ + `Inspect the Runtime with ${commandHint(["runtime", "status"], options.commandScope)}; `
166
+ + `read execution evidence with ${commandHint(["logs", build], options.commandScope)}`,
163
167
  );
164
168
  }
165
169
  }
@@ -184,6 +188,7 @@ export async function observeBuild(
184
188
  readonly controller?: CliRuntimeController;
185
189
  readonly readResult: () => Promise<BuildResultManifest | undefined>;
186
190
  readonly onProgress?: (value: BuildProgressView) => void;
191
+ readonly commandScope?: CommandScope;
187
192
  },
188
193
  ): Promise<CliBuildSubmission> {
189
194
  const finishFromResult = async (): Promise<CliBuildSubmission> => {
@@ -3,6 +3,7 @@ import { relative, resolve } from "node:path";
3
3
  import type { CanonicalValue } from "@hypit/protocol";
4
4
 
5
5
  import type { OperationalMachineView } from "./machine-view.js";
6
+ import { CliUsageError } from "./usage-error.js";
6
7
 
7
8
  export type CliTerminal = {
8
9
  readonly isTTY: boolean;
@@ -970,6 +971,9 @@ function commandHelp(topic: string, colors: Palette): readonly string[] | undefi
970
971
  " hypit auth login <endpoint-instance> [--runtime <profile>] [--slot <name>] [--from <secret-file>]",
971
972
  " hypit auth logout <endpoint-instance> [--runtime <profile>] [--slot <name>]",
972
973
  " All auth actions accept --workspace <project> to use that project's selection.",
974
+ " status shows credential presence and the Provider's declared browser login, if any.",
975
+ " login opens that browser flow or securely prompts for the secret; --from imports a secret instead.",
976
+ " Configure the service's Endpoint first. Storing a key does not install a Provider or select bindings.",
973
977
  ],
974
978
  };
975
979
  const selected = topics[topic];
@@ -986,6 +990,7 @@ export function writeCliHelp(io: CliIo, topic?: string): void {
986
990
  io.write(`${selected.join("\n")}\n`);
987
991
  return;
988
992
  }
993
+ throw new CliUsageError(`Unknown help topic ${JSON.stringify(topic)}`, "hypit help");
989
994
  }
990
995
  io.write([
991
996
  colors.accent(colors.strong("Hypit")),
@@ -1044,6 +1049,7 @@ export function renderCliError(error: unknown, options: {
1044
1049
  error: {
1045
1050
  code,
1046
1051
  message: source.message,
1052
+ ...(source instanceof CliUsageError ? { help: source.help } : {}),
1047
1053
  ...(options.debug && trace !== undefined && trace.length > 0 ? { trace } : {}),
1048
1054
  },
1049
1055
  }, null, 2)}\n`;
@@ -1058,6 +1064,8 @@ export function renderCliError(error: unknown, options: {
1058
1064
  ];
1059
1065
  if (options.debug && trace !== undefined && trace.length > 0) {
1060
1066
  lines.push("", colors.dim(trace));
1067
+ } else if (source instanceof CliUsageError) {
1068
+ lines.push("", `Usage ${source.help}`);
1061
1069
  } else {
1062
1070
  lines.push("", colors.dim("Run with --debug to include the internal stack trace."));
1063
1071
  }
@@ -0,0 +1,8 @@
1
+ /** Argument errors point to public usage rather than an internal stack trace. */
2
+ export class CliUsageError extends Error {
3
+ readonly code = "CLI_USAGE";
4
+ constructor(message: string, readonly help: string, options?: ErrorOptions) {
5
+ super(message, options);
6
+ this.name = "CliUsageError";
7
+ }
8
+ }
@@ -8,6 +8,8 @@ import type {
8
8
  import { buildIdCreatedAt } from "@hypit/protocol";
9
9
  import type { TypeRef } from "@hypit/protocol";
10
10
  import type { BuildView } from "@hypit/runtime-host-node";
11
+ import { commandHint } from "./command-hint.js";
12
+ import type { CommandScope } from "./command-hint.js";
11
13
 
12
14
  export type PublicOutputKind = "scalar" | "resource" | "composite";
13
15
 
@@ -159,6 +161,7 @@ export function buildStatusView(options: {
159
161
  readonly result?: BuildResultManifest;
160
162
  readonly resultReadError?: string;
161
163
  readonly verbose?: boolean;
164
+ readonly commandScope?: CommandScope;
162
165
  }): CliBuildStatusView {
163
166
  const resultState = options.resultReadError !== undefined
164
167
  ? "unavailable" as const
@@ -195,7 +198,7 @@ export function buildStatusView(options: {
195
198
  attention: issue !== undefined
196
199
  ? {
197
200
  message: issue.message,
198
- ...(issue.scope === "result" ? { action: `hypit result finish ${options.id}` } : {}),
201
+ action: commandHint(["result", "finish", options.id], options.commandScope),
199
202
  }
200
203
  : { message: options.resultReadError! },
201
204
  }),
@@ -17,6 +17,11 @@ Build. Credentials are resolved only for slots declared by the selected endpoint
17
17
  of the same credential are coalesced. Cancellation is best effort and never rolls back completed work.
18
18
  A failed action retains its receipt and error for the Result; the Driver does not retry or reconcile it.
19
19
 
20
+ Immediate calls and asynchronous submit, poll and collect actions receive the same progress and
21
+ diagnostic callbacks. Progress reaches the caller while the action is running; changes of phase also
22
+ enter the execution log. The Endpoint supplies the content, and the Driver forwards it with the
23
+ selected Endpoint identity without interpreting service-specific phases or treating progress as a receipt.
24
+
20
25
  `acceptOperation()` validates an already received value without making a remote call. Runtime can
21
26
  retain such a sibling result when another Need fails, without polling unfinished jobs to completion.
22
27
  The Driver validates returned values before offering a command result to Core. It does not load
@@ -22,6 +22,7 @@ import type {
22
22
  CredentialStore,
23
23
  CredentialValue,
24
24
  OperationSnapshot,
25
+ OperationProgress,
25
26
  OperationStore,
26
27
  RuntimeExecutionContext,
27
28
  RuntimeExecutionResult,
@@ -349,6 +350,23 @@ export class NodeDriver {
349
350
  return result;
350
351
  }
351
352
 
353
+ #endpointObservers(endpoint: string, context?: RuntimeExecutionContext) {
354
+ let phase: string | undefined;
355
+ return {
356
+ reportProgress: async (progress: OperationProgress) => {
357
+ await context?.reportProgress?.({ endpoint, progress });
358
+ if (progress.phase !== phase) {
359
+ phase = progress.phase;
360
+ await context?.recordExecution?.({ endpoint, kind: "phase", phase });
361
+ }
362
+ },
363
+ ...(context?.recordExecution === undefined ? {} : {
364
+ reportDiagnostic: (diagnostic: import("@hypit/runtime").ExecutionDiagnostic) =>
365
+ context.recordExecution!({ endpoint, kind: "diagnostic", ...diagnostic }),
366
+ }),
367
+ };
368
+ }
369
+
352
370
  async #advanceOperationStep(
353
371
  operation: OperationSnapshot,
354
372
  registration: EndpointRegistration,
@@ -374,10 +392,7 @@ export class NodeDriver {
374
392
  command: structuredClone(command), need: structuredClone(command.need),
375
393
  resources: this.#resourceStore(operation.build), operation: operation.id,
376
394
  credentials: await this.#endpointCredentials({ ...registration, credentials: operation.credentials ?? registration.credentials ?? {} }),
377
- ...(runtimeContext.recordExecution === undefined ? {} : {
378
- reportDiagnostic: (diagnostic: import("@hypit/runtime").ExecutionDiagnostic) =>
379
- runtimeContext.recordExecution!({ endpoint: operation.endpoint, kind: "diagnostic", ...diagnostic }),
380
- }),
395
+ ...this.#endpointObservers(operation.endpoint, runtimeContext),
381
396
  checkpoint: async (checkpoint: import("@hypit/endpoint-kit").EndpointCheckpoint) => {
382
397
  await operations.update(operation.id, { status: "pending", ...checkpoint,
383
398
  submission: "accepted", acknowledgedAt: operation.acknowledgedAt ?? Date.now(), wakeAt: Date.now(), progress: { phase: checkpoint.remoteEnded ? "collecting" : "submitted" } });
@@ -493,7 +508,6 @@ export class NodeDriver {
493
508
  );
494
509
  }
495
510
  }
496
- let phase: string | undefined;
497
511
  await context?.recordExecution?.({ endpoint: executable.endpointId, kind: "started" });
498
512
  try {
499
513
  const result = await executable.registration.handler({
@@ -501,16 +515,7 @@ export class NodeDriver {
501
515
  need: structuredClone(executable.command.need),
502
516
  resources: this.#resourceStore(context?.build),
503
517
  credentials: await this.#endpointCredentials(executable.registration),
504
- reportProgress: async (progress) => {
505
- await context?.reportProgress?.({ endpoint: executable.endpointId, progress });
506
- if (progress.phase !== phase) {
507
- phase = progress.phase;
508
- await context?.recordExecution?.({ endpoint: executable.endpointId, kind: "phase", phase });
509
- }
510
- },
511
- ...(context?.recordExecution === undefined ? {} : {
512
- reportDiagnostic: (diagnostic) => context.recordExecution!({ endpoint: executable.endpointId, kind: "diagnostic", ...diagnostic }),
513
- }),
518
+ ...this.#endpointObservers(executable.endpointId, context),
514
519
  });
515
520
  const event = await this.#endpointEvent(state, executable, result);
516
521
  await context?.recordExecution?.({ endpoint: executable.endpointId, kind: "completed" });
@@ -35,12 +35,21 @@ without changing Core demand. Each resource declares a `limit` and optional `uni
35
35
  A capability may add `resources` and a pure `unitsForRequest(request)` resolver for quantities of
36
36
  already declared resources. Runtime admits all claims atomically for one `fulfill-need` Command.
37
37
 
38
- A long immediate call can report its current activity with
38
+ A running immediate call or asynchronous action can report its current activity with
39
39
  `await context.reportProgress?.({ phase: "processing", completed: 12, total: 40, unit: "items" })`.
40
40
  The Provider chooses meaningful phases and quantities and reports non-secret, human-readable facts.
41
41
  The Runtime attaches them to the currently executing Command; they do not change its outcome,
42
42
  scheduling or Core facts. Completion clears the live activity. Direct callers may omit the callback.
43
- For asynchronous work, return `progress` in the existing pending outcome instead.
43
+ For asynchronous work, these callbacks describe work inside `start`, `poll` or `collect`, such as
44
+ preparing references or downloading results. A returned `pending.progress` describes the acknowledged
45
+ remote task between actions. Neither form of progress substitutes for a received task ID or receipt.
46
+
47
+ Separate facts available from the request from actions needed to fulfill it. Check known input
48
+ limits and determine the requested service operation before transferring its resources. If the
49
+ service exposes account-specific capability information, use that evidence for the selected request;
50
+ an API without such a query needs no invented discovery step. A Provider owns the meaning of its
51
+ public error codes and reasons. Preserve the failed operation and that evidence, distinguishing what
52
+ was never submitted from a submission whose remote outcome is unknown.
44
53
 
45
54
  Asynchronous execution moves forward through `start`, `poll`, and optional `collect`. `start` returns
46
55
  a task handle; `pending` means an acknowledged task is still running. `ready` records remote completion
@@ -101,7 +101,7 @@ export type EndpointInvocationContext = {
101
101
  readonly credentials: Readonly<Record<string, EndpointCredential>>;
102
102
  /** Explicit non-secret diagnostic output. The Provider owns its content, never its storage. */
103
103
  readonly reportDiagnostic?: (diagnostic: import("@hypit/runtime").ExecutionDiagnostic) => Promise<void>;
104
- /** Report non-secret activity of a long local call without creating a remote Operation. */
104
+ /** Report non-secret activity while an immediate call or asynchronous action is still running. */
105
105
  readonly reportProgress?: (progress: OperationProgress) => Promise<void>;
106
106
  };
107
107
 
@@ -9,6 +9,16 @@ implementations. `sealGenerationPortTable` describes a model's inputs; `Generati
9
9
  `selectWireModelForRequest` applies the same route selection without resolving media bytes. During
10
10
  planning, a Provider may add the model-port names already attached as future graph inputs; the
11
11
  selector does not inspect graph structure or interpret media roles.
12
+ Route selection here applies declared input-mode mappings for the already chosen exact capability;
13
+ it does not search for an alternative model, service or account.
14
+
15
+ `compileWireRequest` awaits its media URL resolver, so compilation can have external effects when
16
+ that resolver uploads files. Use `selectWireModelForRequest` for request identity and known-port
17
+ checks before those effects; pricing and execution can share the same mapping. A Provider can also
18
+ describe its API operation from the authored ports. Service-specific support queries belong to that
19
+ Provider and only use operations its API actually exposes. The generic helper neither queries a
20
+ catalogue nor switches to another route after a failure. Actual URL resolution belongs to request
21
+ execution, not to discovering a model name or making a planning placeholder.
12
22
 
13
23
  The package owns `GenerationRequest`, `GeneratedImageSet` and `GeneratedVideoSet` identities,
14
24
  schemas, validators and graph facets. Generated sets are atomic Products: a Provider persists the
@@ -16,7 +16,7 @@ admits them into the current Build's working byte area. Image references use Hyp
16
16
  `reference_image_urls`, `reference_videos`, and `reference_audios` fields. A single reference video
17
17
  remains in `reference_videos`; `ref_video_url` is reserved for a model's source-video port. First/last-frame images use `first_frame` and `last_frame`.
18
18
 
19
- Seedance 2.5 (`@hypit/seedance` model `2.5`) maps to `bytedance/seedance-2-5` and supports
19
+ Seedance 2.5 (`@hypit/seedance` model `2.5`) maps to `seedance-2.5` and supports
20
20
  `480p`, `720p` and `1080p`. The Provider passes the authored `resolution` to `POST /v1/videos`;
21
21
  omitting it in the Seedance Surface defaults to `720p`.
22
22
 
@@ -31,11 +31,44 @@ The current HypiHub GPT Image 2 route has these service-specific limits:
31
31
  HypiHub owns this support check independently: it leaves the GPT Image model package unchanged. When the service surface changes, this Provider can change without changing
32
32
  the model or another Provider.
33
33
 
34
- Model identity is preserved across input modes: Seedream 5 Lite references use the Lite image-edit
35
- route, and Grok 1.5 Preview remains the Preview model. A deployment's current catalogue may offer
34
+ Model identity and input mode are separate. The mapping uses HypiHub's canonical model names:
35
+ `gpt-image-2`, `seedream-5-lite`, `minimax-h3`, `grok-imagine-video` and the individual Seedance names.
36
+ An image request without references uses `/images/generations`; image edits use `/images/edits`
37
+ with the same model name. Video requests use `/videos`, preserving reference images, reference
38
+ videos and first/last frames in their distinct fields. Old operation-specific names are not needed
39
+ to express these modes; compatibility with previously released clients belongs to the service.
40
+
41
+ Seedream 5 Lite remains Lite across input modes, and Grok 1.5 Preview remains Preview.
42
+ A deployment's current catalogue may offer
36
43
  newer models or omit one implemented here. Availability and unsupported-input errors retain their
37
44
  service explanation; they do not imply expired credentials or authorize substituting another model.
38
45
 
46
+ Before resolving or uploading references, the Provider prepares the exact model and operation from
47
+ the authored ports and its mapping, then queries that model's authenticated directory entry. Image
48
+ editing is determined from the mapped media inputs, without manufacturing placeholder URLs or
49
+ uploading to discover the request mode. The same preparation serves images, videos and speech.
50
+ The request is then translated with real reference URLs and submitted to the selected operation.
51
+ These are internal Provider functions; Author Sources, CLI commands and Runtime scheduling are unchanged.
52
+
53
+ Progress identifies the directory query, request preparation and submission. A directory failure
54
+ retains the model, operation and service evidence, and states that this invocation uploaded no
55
+ references and submitted no generation. A missing or malformed operation list leaves support unknown;
56
+ an explicit list without the requested operation reports the actual list. No alternative model,
57
+ operation or account is attempted. Directory support alone does not establish balance, every input
58
+ combination or eventual generation success. Preparation failure also reports that generation was not
59
+ submitted; an interrupted submission retains its actual evidence without claiming no remote work exists.
60
+
61
+ The Provider interprets HypiHub's [HTTP errors](https://hypit.ai/api-reference/errors/) and
62
+ [job errors](https://hypit.ai/api-reference/jobs/) locally. Failures retain the service code,
63
+ HTTP method/route/status, requested model and `X-Request-Id` when available, plus the public reason.
64
+ `Retry-After` remains evidence and does not start another generation attempt. Job failures retain
65
+ `error_code`, `error` and their receipt even when the create response is already terminal.
66
+ Known error messages are not cut to a fixed prefix; only an unstructured non-JSON response uses
67
+ a marked excerpt. Unrelated response fields and signed asset URLs are not diagnostic content.
68
+ Runtime and Result retain ordinary failure codes/messages without interpreting HypiHub fields.
69
+ Immediate Endpoint exceptions keep the same evidence in their message. A `401` alone does not
70
+ choose OAuth over API-key configuration or establish that another login will fix the account.
71
+
39
72
  For moving portraits, [Volcengine Matting](../volcengine-matting/README.md) maps
40
73
  `@hypit/volcengine-matting@1#matte-portrait-video` to `POST /v1/videos` with
41
74
  `model: "matte-portrait-video"`, `ref_video_url` and `format` (`WEBM` by default, or `MOV`).
@@ -122,8 +155,10 @@ catalog to verify configured capabilities; ordinary preflight never makes that r
122
155
  declares HypiHub's public pricing page, `https://hypit.ai/commercial/pricing/`, as its price source.
123
156
  For each selected Need, `readPricing` resolves the corresponding HypiHub model and returns the service's
124
157
  authenticated `GET /v1/pricing?model=<model>` response unchanged together with that URL. It covers
125
- generation, alignment, Voice Design and Voice Clone through the same mechanism;
126
- the Provider does not maintain a second list of billing formulas or calculate a request total.
158
+ generation, alignment, Voice Design and Voice Clone through the same mechanism. The document's
159
+ per-operation prices are retained alongside its default price, including when references are still
160
+ pending. A model's default price is not a quote for every input mode. The Provider does not maintain
161
+ a second list of billing formulas or calculate a request total.
127
162
 
128
163
  The Provider declares its implemented speech capabilities alongside image, video and alignment.
129
164
  Voice Design produces an accepted voice-reference Resource, and Voice Clone uses that
@@ -148,7 +183,7 @@ Execution policy remains local to this Provider:
148
183
  | `requestTimeoutMs` | 300 seconds | ordinary Provider HTTP requests |
149
184
  | `oauthRequestTimeoutMs` | 30 seconds | OAuth token exchange and refresh |
150
185
  | `pricingRequestTimeoutMs` | 30 seconds | authenticated pricing requests |
151
- | `operationTimeoutMs` | 20 minutes | one remote asynchronous operation |
186
+ | `operationTimeoutMs` | 20 minutes | how long this Provider observes one asynchronous operation; expiry does not cancel the remote job |
152
187
  | `uploadConcurrency` | 8 | whole file sessions per origin/credential within this process |
153
188
  | `uploadPartTimeoutMs` | 5 minutes | one upload part |
154
189
  | `uploadPartAttempts` | 3 | attempts for one upload part |
@@ -0,0 +1,61 @@
1
+ /** HypiHub's public error envelope, kept at the service boundary. */
2
+ function record(value: unknown): Record<string, unknown> | undefined {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value)
4
+ ? value as Record<string, unknown> : undefined;
5
+ }
6
+ function text(value: unknown): string | undefined {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
8
+ }
9
+
10
+ // Responses may mention a signed asset URL. Keep the reason, not its access capability.
11
+ export function safeHypiHubReason(value: string): string {
12
+ return value.replace(/https?:\/\/\S+/giu, "[redacted-url]");
13
+ }
14
+
15
+ export class HypiHubServiceError extends Error {
16
+ constructor(readonly code: string, message: string) { super(message); }
17
+ }
18
+
19
+ export class HypiHubHttpError extends HypiHubServiceError {
20
+ readonly retryAfterMs?: number;
21
+ constructor(readonly status: number, response: { readonly headers: Headers }, bodyText: string,
22
+ request: { readonly method: string; readonly url: string; readonly model?: string }) {
23
+ let body: Record<string, unknown> | undefined;
24
+ try { body = record(JSON.parse(bodyText)); } catch { /* Non-JSON gateway failures still have HTTP evidence. */ }
25
+ const error = record(body?.error);
26
+ const flatError = text(body?.error);
27
+ const code = text(error?.code)
28
+ ?? (flatError !== undefined && /^[a-z][a-z0-9_]*$/iu.test(flatError) ? flatError : "HYPIHUB_HTTP_ERROR");
29
+ const reason = text(error?.message) ?? text(body?.error_description)
30
+ ?? (flatError !== code ? flatError : undefined)
31
+ ?? (body === undefined ? text(bodyText.slice(0, 2000)) : undefined);
32
+ const url = new URL(request.url);
33
+ const model = request.model ?? url.searchParams.get("model")
34
+ ?? (url.pathname.includes("/models/") ? decodeURIComponent(url.pathname.split("/models/")[1]!) : undefined);
35
+ const requestId = text(response.headers.get("x-request-id"));
36
+ const retryAfter = text(response.headers.get("retry-after"));
37
+ const facts = [
38
+ `HypiHub HTTP ${status}`, code, `${request.method} ${url.origin}${url.pathname}`,
39
+ ...(model == null ? [] : [`model=${model}`]),
40
+ ...(requestId === undefined ? [] : [`request=${requestId}`]),
41
+ ...(retryAfter === undefined ? [] : [`retry-after=${retryAfter}`]),
42
+ ];
43
+ super(code, `${facts.join("; ")}${reason === undefined ? "" : `: ${safeHypiHubReason(reason)}`}`
44
+ + (body === undefined && bodyText.length > 2000 ? " [response excerpt truncated]" : ""));
45
+ const delay = retryAfter === undefined ? NaN : /^\d+$/u.test(retryAfter)
46
+ ? Number(retryAfter) * 1000 : Date.parse(retryAfter) - Date.now();
47
+ if (Number.isFinite(delay)) this.retryAfterMs = Math.max(0, delay);
48
+ }
49
+ }
50
+
51
+ export function hypiHubJobFailure(job: Record<string, unknown>, id: string): HypiHubServiceError | undefined {
52
+ const status = job.status;
53
+ if (!["failed", "queue_expired", "canceled", "cancelled"].includes(String(status))) return undefined;
54
+ const error = record(job.error);
55
+ const code = text(job.error_code) ?? text(error?.code) ?? "HYPIHUB_JOB_FAILED";
56
+ const reason = text(job.error) ?? text(error?.message) ?? text(job.message) ?? text(job.reason) ?? text(job.detail);
57
+ const model = text(job.model);
58
+ return new HypiHubServiceError(code,
59
+ `HypiHub job ${id} ${status}; ${code}${model === undefined ? "" : `; model=${model}`}`
60
+ + (reason === undefined ? "" : `: ${safeHypiHubReason(reason)}`));
61
+ }
@@ -12,8 +12,8 @@ const MIMO_SPEECH: ModuleRef = { name: "@hypit/mimo-speech", version: "1" };
12
12
  const FISHAUDIO_SPEECH: ModuleRef = { name: "@hypit/fishaudio-speech", version: "1" };
13
13
  const ELEVENLABS_SPEECH: ModuleRef = { name: "@hypit/elevenlabs-speech", version: "1" };
14
14
 
15
- const seedance = (name: string, model: string): GenerationWireMapping => ({
16
- capability: { module: SEEDANCE, name }, result: "video", routes: [{ model }],
15
+ const seedance = (name: string): GenerationWireMapping => ({
16
+ capability: { module: SEEDANCE, name }, result: "video", routes: [{ model: name }],
17
17
  fields: {
18
18
  prompt: { as: "value", field: "prompt" },
19
19
  referenceImage: { as: "urlArray", field: "reference_image_urls", resourceFields: ["personReference"] },
@@ -38,15 +38,12 @@ export const hypiHubMappings: readonly GenerationWireMapping[] = [
38
38
  format: { as: "value", field: "format", whenAbsent: "WEBM" },
39
39
  },
40
40
  },
41
- seedance("seedance-2", "bytedance/seedance-2"),
42
- seedance("seedance-2-fast", "bytedance/seedance-2-fast"),
43
- seedance("seedance-2-mini", "bytedance/seedance-2-mini"),
44
- seedance("seedance-2.5", "bytedance/seedance-2-5"),
41
+ seedance("seedance-2"),
42
+ seedance("seedance-2-fast"),
43
+ seedance("seedance-2-mini"),
44
+ seedance("seedance-2.5"),
45
45
  {
46
- capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image", routes: [
47
- { model: "gpt-image-2-image-to-image", whenPresent: ["images"] },
48
- { model: "gpt-image-2-text-to-image" },
49
- ],
46
+ capability: { module: GPT_IMAGE, name: "gpt-image-2" }, result: "image", routes: [{ model: "gpt-image-2" }],
50
47
  fields: {
51
48
  prompt: { as: "value", field: "prompt" },
52
49
  aspectRatio: { as: "value", field: "aspect_ratio" },
@@ -66,10 +63,7 @@ export const hypiHubMappings: readonly GenerationWireMapping[] = [
66
63
  },
67
64
  })),
68
65
  {
69
- capability: { module: SEEDREAM, name: "seedream-5-lite" }, result: "image", routes: [
70
- { model: "seedream/5-lite-image-to-image", whenPresent: ["images"] },
71
- { model: "seedream/5-lite-text-to-image" },
72
- ],
66
+ capability: { module: SEEDREAM, name: "seedream-5-lite" }, result: "image", routes: [{ model: "seedream-5-lite" }],
73
67
  fields: {
74
68
  prompt: { as: "value", field: "prompt" },
75
69
  aspectRatio: { as: "value", field: "aspect_ratio" },
@@ -80,13 +74,7 @@ export const hypiHubMappings: readonly GenerationWireMapping[] = [
80
74
  },
81
75
  },
82
76
  {
83
- capability: { module: MINIMAX, name: "minimax-h3" }, result: "video", routes: [
84
- { model: "minimax-h3/image-to-video", whenPresent: ["lastFrame"] },
85
- { model: "minimax-h3/image-to-video", whenPresent: ["firstFrame"] },
86
- { model: "minimax-h3/reference-to-video", whenPresent: ["referenceImage"] },
87
- { model: "minimax-h3/reference-to-video", whenPresent: ["referenceVideo"] },
88
- { model: "minimax-h3/text-to-video" },
89
- ],
77
+ capability: { module: MINIMAX, name: "minimax-h3" }, result: "video", routes: [{ model: "minimax-h3" }],
90
78
  fields: {
91
79
  prompt: { as: "value", field: "prompt" }, duration: { as: "value", field: "seconds" },
92
80
  resolution: { as: "value", field: "resolution", whenAbsent: "2k" }, aspectRatio: { as: "value", field: "aspect_ratio" },
@@ -98,10 +86,7 @@ export const hypiHubMappings: readonly GenerationWireMapping[] = [
98
86
  },
99
87
  },
100
88
  {
101
- capability: { module: GROK, name: "grok-imagine-video" }, result: "video", routes: [
102
- { model: "grok-imagine/image-to-video", whenPresent: ["images"] },
103
- { model: "grok-imagine/text-to-video" },
104
- ],
89
+ capability: { module: GROK, name: "grok-imagine-video" }, result: "video", routes: [{ model: "grok-imagine-video" }],
105
90
  fields: {
106
91
  prompt: { as: "value", field: "prompt" }, duration: { as: "value", field: "seconds" },
107
92
  resolution: { as: "value", field: "resolution" }, aspectRatio: { as: "value", field: "aspect_ratio" },