@sanity/ailf 4.4.0 → 4.6.0

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 (34) hide show
  1. package/dist/_vendor/ailf-core/artifact-registry.d.ts +138 -1
  2. package/dist/_vendor/ailf-core/artifact-registry.js +137 -4
  3. package/dist/_vendor/ailf-core/ports/context.d.ts +18 -0
  4. package/dist/_vendor/ailf-core/ports/index.d.ts +2 -0
  5. package/dist/_vendor/ailf-core/ports/index.js +1 -0
  6. package/dist/_vendor/ailf-core/ports/llm-client.d.ts +112 -0
  7. package/dist/_vendor/ailf-core/ports/llm-client.js +68 -0
  8. package/dist/_vendor/ailf-core/types/confidence.d.ts +68 -0
  9. package/dist/_vendor/ailf-core/types/confidence.js +49 -0
  10. package/dist/_vendor/ailf-core/types/index.d.ts +2 -0
  11. package/dist/_vendor/ailf-core/types/index.js +1 -0
  12. package/dist/adapters/llm/anthropic-llm-client.d.ts +48 -0
  13. package/dist/adapters/llm/anthropic-llm-client.js +205 -0
  14. package/dist/adapters/llm/fake-llm-client.d.ts +49 -0
  15. package/dist/adapters/llm/fake-llm-client.js +63 -0
  16. package/dist/adapters/llm/index.d.ts +9 -0
  17. package/dist/adapters/llm/index.js +4 -0
  18. package/dist/adapters/llm/openai-llm-client.d.ts +44 -0
  19. package/dist/adapters/llm/openai-llm-client.js +168 -0
  20. package/dist/adapters/llm/pricing.d.ts +12 -0
  21. package/dist/adapters/llm/pricing.js +8 -0
  22. package/dist/adapters/llm/retry.d.ts +56 -0
  23. package/dist/adapters/llm/retry.js +66 -0
  24. package/dist/adapters/task-sources/repo-schemas.d.ts +11 -11
  25. package/dist/artifact-capture/api-gateway-artifact-writer.js +2 -1
  26. package/dist/artifact-capture/batching-api-gateway-artifact-writer.js +2 -1
  27. package/dist/artifact-capture/gcs-artifact-writer.js +3 -1
  28. package/dist/artifact-capture/local-fs-artifact-writer.js +3 -1
  29. package/dist/commands/pipeline-action.js +7 -1
  30. package/dist/commands/run.d.ts +1 -0
  31. package/dist/commands/run.js +1 -0
  32. package/dist/composition-root.d.ts +23 -1
  33. package/dist/composition-root.js +47 -0
  34. package/package.json +3 -3
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Per-model pricing types shared by LLM adapters.
3
+ *
4
+ * Hard-coded vendor pricing drifts; treat the in-adapter defaults as a
5
+ * sensible starting point and override via constructor options when the
6
+ * vendor changes their rate card.
7
+ */
8
+ export {};
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared retry helper for LLMClient adapters.
3
+ *
4
+ * Bounded exponential backoff with optional `Retry-After` honoring and
5
+ * symmetric jitter. Treats 429 / 5xx as retryable and any other HTTP error
6
+ * as terminal.
7
+ *
8
+ * Errors carry the full response body on the instance for callers that need
9
+ * to inspect it; the message is intentionally short and body-free so it's
10
+ * safe to include in user-facing logs and stack traces.
11
+ */
12
+ export interface RetryPolicy {
13
+ /** Total attempts including the initial call. Default 3. */
14
+ maxAttempts: number;
15
+ /** Initial backoff in ms. Default 500. */
16
+ baseDelayMs: number;
17
+ /** Multiplier per attempt. Default 2. */
18
+ backoffFactor: number;
19
+ /** Cap on a single delay in ms. Default 10_000. */
20
+ maxDelayMs: number;
21
+ /**
22
+ * Symmetric jitter as a fraction of the computed delay, in `[0, 1)`. The
23
+ * actual delay is `delay * (1 + (rng() - 0.5) * 2 * jitter)`. Default 0.3.
24
+ * Set to 0 to disable.
25
+ */
26
+ jitter: number;
27
+ }
28
+ export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
29
+ export declare class LLMHttpError extends Error {
30
+ readonly status: number;
31
+ readonly attempts: number;
32
+ /** Full upstream response body (kept on the instance, NOT in `message`). */
33
+ readonly body: string;
34
+ constructor(status: number, body: string, attempts: number);
35
+ }
36
+ export declare function isRetryableStatus(status: number): boolean;
37
+ export interface RunWithRetryArgs<T> {
38
+ policy: RetryPolicy;
39
+ /** Per-attempt callable. Resolves to {result} on success, or returns ok:false to fail. */
40
+ attempt: () => Promise<{
41
+ ok: true;
42
+ value: T;
43
+ } | {
44
+ ok: false;
45
+ status: number;
46
+ body: string;
47
+ retryAfterSeconds?: number;
48
+ }>;
49
+ /** Sleeps for `ms`. Injectable for tests. */
50
+ sleep?: (ms: number) => Promise<void>;
51
+ /** Random source in `[0, 1)`. Injectable for tests. Defaults to `Math.random`. */
52
+ rng?: () => number;
53
+ }
54
+ export declare function runWithRetry<T>(args: RunWithRetryArgs<T>): Promise<T>;
55
+ /** Parses a `Retry-After` header (seconds-only form). */
56
+ export declare function parseRetryAfterSeconds(header: null | string): number | undefined;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared retry helper for LLMClient adapters.
3
+ *
4
+ * Bounded exponential backoff with optional `Retry-After` honoring and
5
+ * symmetric jitter. Treats 429 / 5xx as retryable and any other HTTP error
6
+ * as terminal.
7
+ *
8
+ * Errors carry the full response body on the instance for callers that need
9
+ * to inspect it; the message is intentionally short and body-free so it's
10
+ * safe to include in user-facing logs and stack traces.
11
+ */
12
+ export const DEFAULT_RETRY_POLICY = {
13
+ maxAttempts: 3,
14
+ baseDelayMs: 500,
15
+ backoffFactor: 2,
16
+ maxDelayMs: 10_000,
17
+ jitter: 0.3,
18
+ };
19
+ export class LLMHttpError extends Error {
20
+ status;
21
+ attempts;
22
+ /** Full upstream response body (kept on the instance, NOT in `message`). */
23
+ body;
24
+ constructor(status, body, attempts) {
25
+ super(`LLM request failed with status ${status} after ${attempts} attempt(s)`);
26
+ this.status = status;
27
+ this.attempts = attempts;
28
+ this.name = "LLMHttpError";
29
+ this.body = body;
30
+ }
31
+ }
32
+ export function isRetryableStatus(status) {
33
+ return status === 429 || (status >= 500 && status < 600);
34
+ }
35
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
36
+ export async function runWithRetry(args) {
37
+ const { policy, attempt, sleep = defaultSleep, rng = Math.random } = args;
38
+ for (let i = 1; i <= policy.maxAttempts; i++) {
39
+ const res = await attempt();
40
+ if (res.ok)
41
+ return res.value;
42
+ const canRetry = i < policy.maxAttempts && isRetryableStatus(res.status);
43
+ if (!canRetry) {
44
+ throw new LLMHttpError(res.status, res.body, i);
45
+ }
46
+ const exp = policy.baseDelayMs * Math.pow(policy.backoffFactor, i - 1);
47
+ const base = res.retryAfterSeconds ? res.retryAfterSeconds * 1000 : exp;
48
+ const capped = Math.min(base, policy.maxDelayMs);
49
+ const jittered = policy.jitter > 0
50
+ ? capped * (1 + (rng() - 0.5) * 2 * policy.jitter)
51
+ : capped;
52
+ await sleep(Math.max(0, Math.round(jittered)));
53
+ }
54
+ // Unreachable: the canRetry branch always throws on the final attempt.
55
+ // Defensive throw so the type checker sees a definite return.
56
+ throw new LLMHttpError(0, "no error body", policy.maxAttempts);
57
+ }
58
+ /** Parses a `Retry-After` header (seconds-only form). */
59
+ export function parseRetryAfterSeconds(header) {
60
+ if (!header)
61
+ return undefined;
62
+ const n = Number(header);
63
+ if (Number.isFinite(n) && n >= 0)
64
+ return n;
65
+ return undefined;
66
+ }
@@ -77,6 +77,7 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
77
77
  weight: z.ZodOptional<z.ZodNumber>;
78
78
  }, z.core.$strip>, z.ZodObject<{
79
79
  type: z.ZodEnum<{
80
+ cost: "cost";
80
81
  "llm-rubric": "llm-rubric";
81
82
  contains: "contains";
82
83
  "contains-any": "contains-any";
@@ -87,7 +88,6 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
87
88
  regex: "regex";
88
89
  javascript: "javascript";
89
90
  similar: "similar";
90
- cost: "cost";
91
91
  latency: "latency";
92
92
  "file-exists": "file-exists";
93
93
  "file-contains": "file-contains";
@@ -191,6 +191,7 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
191
191
  weight: z.ZodOptional<z.ZodNumber>;
192
192
  }, z.core.$strip>, z.ZodObject<{
193
193
  type: z.ZodEnum<{
194
+ cost: "cost";
194
195
  "llm-rubric": "llm-rubric";
195
196
  contains: "contains";
196
197
  "contains-any": "contains-any";
@@ -201,7 +202,6 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
201
202
  regex: "regex";
202
203
  javascript: "javascript";
203
204
  similar: "similar";
204
- cost: "cost";
205
205
  latency: "latency";
206
206
  "file-exists": "file-exists";
207
207
  "file-contains": "file-contains";
@@ -345,6 +345,7 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
345
345
  weight: z.ZodOptional<z.ZodNumber>;
346
346
  }, z.core.$strip>, z.ZodObject<{
347
347
  type: z.ZodEnum<{
348
+ cost: "cost";
348
349
  "llm-rubric": "llm-rubric";
349
350
  contains: "contains";
350
351
  "contains-any": "contains-any";
@@ -355,7 +356,6 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
355
356
  regex: "regex";
356
357
  javascript: "javascript";
357
358
  similar: "similar";
358
- cost: "cost";
359
359
  latency: "latency";
360
360
  "file-exists": "file-exists";
361
361
  "file-contains": "file-contains";
@@ -476,6 +476,7 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
476
476
  weight: z.ZodOptional<z.ZodNumber>;
477
477
  }, z.core.$strip>, z.ZodObject<{
478
478
  type: z.ZodEnum<{
479
+ cost: "cost";
479
480
  "llm-rubric": "llm-rubric";
480
481
  contains: "contains";
481
482
  "contains-any": "contains-any";
@@ -486,7 +487,6 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
486
487
  regex: "regex";
487
488
  javascript: "javascript";
488
489
  similar: "similar";
489
- cost: "cost";
490
490
  latency: "latency";
491
491
  "file-exists": "file-exists";
492
492
  "file-contains": "file-contains";
@@ -595,6 +595,7 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
595
595
  weight: z.ZodOptional<z.ZodNumber>;
596
596
  }, z.core.$strip>, z.ZodObject<{
597
597
  type: z.ZodEnum<{
598
+ cost: "cost";
598
599
  "llm-rubric": "llm-rubric";
599
600
  contains: "contains";
600
601
  "contains-any": "contains-any";
@@ -605,7 +606,6 @@ export declare const CanonicalTaskSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
605
606
  regex: "regex";
606
607
  javascript: "javascript";
607
608
  similar: "similar";
608
- cost: "cost";
609
609
  latency: "latency";
610
610
  "file-exists": "file-exists";
611
611
  "file-contains": "file-contains";
@@ -703,6 +703,7 @@ export declare const ContentLakeAuthorableTaskSchema: z.ZodObject<{
703
703
  weight: z.ZodOptional<z.ZodNumber>;
704
704
  }, z.core.$strip>, z.ZodObject<{
705
705
  type: z.ZodEnum<{
706
+ cost: "cost";
706
707
  "llm-rubric": "llm-rubric";
707
708
  contains: "contains";
708
709
  "contains-any": "contains-any";
@@ -713,7 +714,6 @@ export declare const ContentLakeAuthorableTaskSchema: z.ZodObject<{
713
714
  regex: "regex";
714
715
  javascript: "javascript";
715
716
  similar: "similar";
716
- cost: "cost";
717
717
  latency: "latency";
718
718
  "file-exists": "file-exists";
719
719
  "file-contains": "file-contains";
@@ -823,6 +823,7 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
823
823
  weight: z.ZodOptional<z.ZodNumber>;
824
824
  }, z.core.$strip>, z.ZodObject<{
825
825
  type: z.ZodEnum<{
826
+ cost: "cost";
826
827
  "llm-rubric": "llm-rubric";
827
828
  contains: "contains";
828
829
  "contains-any": "contains-any";
@@ -833,7 +834,6 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
833
834
  regex: "regex";
834
835
  javascript: "javascript";
835
836
  similar: "similar";
836
- cost: "cost";
837
837
  latency: "latency";
838
838
  "file-exists": "file-exists";
839
839
  "file-contains": "file-contains";
@@ -937,6 +937,7 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
937
937
  weight: z.ZodOptional<z.ZodNumber>;
938
938
  }, z.core.$strip>, z.ZodObject<{
939
939
  type: z.ZodEnum<{
940
+ cost: "cost";
940
941
  "llm-rubric": "llm-rubric";
941
942
  contains: "contains";
942
943
  "contains-any": "contains-any";
@@ -947,7 +948,6 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
947
948
  regex: "regex";
948
949
  javascript: "javascript";
949
950
  similar: "similar";
950
- cost: "cost";
951
951
  latency: "latency";
952
952
  "file-exists": "file-exists";
953
953
  "file-contains": "file-contains";
@@ -1091,6 +1091,7 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
1091
1091
  weight: z.ZodOptional<z.ZodNumber>;
1092
1092
  }, z.core.$strip>, z.ZodObject<{
1093
1093
  type: z.ZodEnum<{
1094
+ cost: "cost";
1094
1095
  "llm-rubric": "llm-rubric";
1095
1096
  contains: "contains";
1096
1097
  "contains-any": "contains-any";
@@ -1101,7 +1102,6 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
1101
1102
  regex: "regex";
1102
1103
  javascript: "javascript";
1103
1104
  similar: "similar";
1104
- cost: "cost";
1105
1105
  latency: "latency";
1106
1106
  "file-exists": "file-exists";
1107
1107
  "file-contains": "file-contains";
@@ -1222,6 +1222,7 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
1222
1222
  weight: z.ZodOptional<z.ZodNumber>;
1223
1223
  }, z.core.$strip>, z.ZodObject<{
1224
1224
  type: z.ZodEnum<{
1225
+ cost: "cost";
1225
1226
  "llm-rubric": "llm-rubric";
1226
1227
  contains: "contains";
1227
1228
  "contains-any": "contains-any";
@@ -1232,7 +1233,6 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
1232
1233
  regex: "regex";
1233
1234
  javascript: "javascript";
1234
1235
  similar: "similar";
1235
- cost: "cost";
1236
1236
  latency: "latency";
1237
1237
  "file-exists": "file-exists";
1238
1238
  "file-contains": "file-contains";
@@ -1341,6 +1341,7 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
1341
1341
  weight: z.ZodOptional<z.ZodNumber>;
1342
1342
  }, z.core.$strip>, z.ZodObject<{
1343
1343
  type: z.ZodEnum<{
1344
+ cost: "cost";
1344
1345
  "llm-rubric": "llm-rubric";
1345
1346
  contains: "contains";
1346
1347
  "contains-any": "contains-any";
@@ -1351,7 +1352,6 @@ export declare const CanonicalTaskFileSchema: z.ZodArray<z.ZodDiscriminatedUnion
1351
1352
  regex: "regex";
1352
1353
  javascript: "javascript";
1353
1354
  similar: "similar";
1354
- cost: "cost";
1355
1355
  latency: "latency";
1356
1356
  "file-exists": "file-exists";
1357
1357
  "file-contains": "file-contains";
@@ -27,7 +27,7 @@
27
27
  * @see docs/decisions/D0032-run-anchored-artifact-store.md
28
28
  * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
29
29
  */
30
- import { ARTIFACT_REGISTRY, NotImplementedError, } from "../_vendor/ailf-core/index.js";
30
+ import { ARTIFACT_REGISTRY, assertWritePolicyMatches, NotImplementedError, } from "../_vendor/ailf-core/index.js";
31
31
  import { prepareUploadBody } from "./prepare-upload-body.js";
32
32
  import { NO_OP_UPLOAD_METRICS, } from "./upload-metrics.js";
33
33
  export class ApiGatewayArtifactWriter {
@@ -40,6 +40,7 @@ export class ApiGatewayArtifactWriter {
40
40
  // ---- Canonical W0049 API ------------------------------------------------
41
41
  async emit(type, association, payload) {
42
42
  const descriptor = ARTIFACT_REGISTRY[type];
43
+ assertWritePolicyMatches("pipeline", descriptor);
43
44
  const runId = association.run;
44
45
  if (!runId) {
45
46
  console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
@@ -25,7 +25,7 @@
25
25
  * does this writer. Traces flow through the GCS-direct writer when ADC
26
26
  * credentials are present.
27
27
  */
28
- import { ARTIFACT_REGISTRY, BULK_ENTRY_KEY, NotImplementedError, } from "../_vendor/ailf-core/index.js";
28
+ import { ARTIFACT_REGISTRY, assertWritePolicyMatches, BULK_ENTRY_KEY, NotImplementedError, } from "../_vendor/ailf-core/index.js";
29
29
  import { prepareUploadBody } from "./prepare-upload-body.js";
30
30
  import { NO_OP_UPLOAD_METRICS, } from "./upload-metrics.js";
31
31
  /**
@@ -64,6 +64,7 @@ export class BatchingApiGatewayArtifactWriter {
64
64
  // ---- ArtifactWriter surface --------------------------------------------
65
65
  async emit(type, association, payload) {
66
66
  const descriptor = ARTIFACT_REGISTRY[type];
67
+ assertWritePolicyMatches("pipeline", descriptor);
67
68
  const runId = association.run;
68
69
  if (!runId) {
69
70
  console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
@@ -28,7 +28,7 @@
28
28
  * @see docs/decisions/D0033-unified-run-anchored-artifact-capture.md
29
29
  */
30
30
  import { Storage } from "@google-cloud/storage";
31
- import { ARTIFACT_REGISTRY, buildManifestPreview, } from "../_vendor/ailf-core/index.js";
31
+ import { ARTIFACT_REGISTRY, assertWritePolicyMatches, buildManifestPreview, } from "../_vendor/ailf-core/index.js";
32
32
  import { resolveUploadConcurrency } from "./parallel-emit.js";
33
33
  import { prepareUploadBody } from "./prepare-upload-body.js";
34
34
  import { redactArtifactData } from "./redact-artifact.js";
@@ -79,6 +79,7 @@ export class GcsArtifactWriter {
79
79
  // ---- Canonical W0049 API ------------------------------------------------
80
80
  async emit(type, association, payload) {
81
81
  const descriptor = ARTIFACT_REGISTRY[type];
82
+ assertWritePolicyMatches("pipeline", descriptor);
82
83
  const runId = association.run;
83
84
  if (!runId) {
84
85
  console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
@@ -132,6 +133,7 @@ export class GcsArtifactWriter {
132
133
  }
133
134
  async appendNdjson(type, association, rows) {
134
135
  const descriptor = ARTIFACT_REGISTRY[type];
136
+ assertWritePolicyMatches("pipeline", descriptor);
135
137
  if (descriptor.mime !== "application/x-ndjson") {
136
138
  console.warn(` ⚠️ appendNdjson("${type}"): descriptor mime is ${descriptor.mime}, not application/x-ndjson — skipping`);
137
139
  return null;
@@ -38,7 +38,7 @@
38
38
  */
39
39
  import { promises as fs } from "node:fs";
40
40
  import path from "node:path";
41
- import { ARTIFACT_REGISTRY, buildManifestPreview, } from "../_vendor/ailf-core/index.js";
41
+ import { ARTIFACT_REGISTRY, assertWritePolicyMatches, buildManifestPreview, } from "../_vendor/ailf-core/index.js";
42
42
  import { redactArtifactData } from "./redact-artifact.js";
43
43
  // ---------------------------------------------------------------------------
44
44
  // Implementation
@@ -66,6 +66,7 @@ export class LocalFilesystemArtifactWriter {
66
66
  if (this.excludeSet.has(type))
67
67
  return null;
68
68
  const descriptor = ARTIFACT_REGISTRY[type];
69
+ assertWritePolicyMatches("pipeline", descriptor);
69
70
  const runId = association.run;
70
71
  if (!runId) {
71
72
  console.warn(` ⚠️ emit("${type}"): association.run is required, skipping`);
@@ -127,6 +128,7 @@ export class LocalFilesystemArtifactWriter {
127
128
  if (this.excludeSet.has(type))
128
129
  return null;
129
130
  const descriptor = ARTIFACT_REGISTRY[type];
131
+ assertWritePolicyMatches("pipeline", descriptor);
130
132
  if (descriptor.mime !== "application/x-ndjson") {
131
133
  console.warn(` ⚠️ appendNdjson("${type}"): descriptor mime is ${descriptor.mime}, not application/x-ndjson — skipping`);
132
134
  return null;
@@ -265,7 +265,13 @@ export function computeResolvedOptions(opts) {
265
265
  const apiUrl = process.env.AILF_API_URL ??
266
266
  repoConfig?.execution?.apiUrl ??
267
267
  "https://ailf-api.sanity.build";
268
- const apiKey = process.env.AILF_API_KEY ?? undefined;
268
+ // W0202 `--api-key` flag wins over the env var. The CLI's dotenv
269
+ // bootstrap (`cli.ts`) loads the project `.env` with `override: true`,
270
+ // matching the repo convention that the file is the source of truth
271
+ // (see `.claude/rules/config.md`). That convention silently squashes
272
+ // shell-set `AILF_API_KEY`, so an explicit per-run flag is the only
273
+ // ergonomic way to pick a non-default key for one invocation.
274
+ const apiKey = opts.apiKey ?? process.env.AILF_API_KEY ?? undefined;
269
275
  // Output directory (W0077 Phase 6c) — `output.dir` from .ailf/config.yaml
270
276
  // when set, otherwise <cwd>/.ailf/results/latest/.
271
277
  const outputDir = resolveOutputDir(repoConfig?.output?.dir);
@@ -60,5 +60,6 @@ export interface PipelineCliOptions {
60
60
  classification?: string;
61
61
  purpose?: string;
62
62
  label: string[];
63
+ apiKey?: string;
63
64
  }
64
65
  export declare function createRunCommand(): Command;
@@ -47,6 +47,7 @@ export function createRunCommand() {
47
47
  .option("-o, --output <path>", "Write PR comment markdown to file")
48
48
  .option("--promptfoo-url <url>", "Promptfoo share URL for report")
49
49
  .option("--remote", "Submit evaluation to the AILF API instead of running locally", false)
50
+ .option("--api-key <value>", "API key for --remote dispatch. Wins over AILF_API_KEY and any value loaded from the project .env file (W0202).")
50
51
  .option("--no-artifacts-write", "Run artifact writers in dry-run mode — log intended writes, touch no storage")
51
52
  // D0037 caller envelope (W0069) — threads through --remote so the
52
53
  // server-side pipeline attributes provenance to the caller, not the
@@ -15,7 +15,7 @@
15
15
  * @see packages/core/src/ports/context.ts — AppContext interface
16
16
  * @see docs/archive/exec-plans/ports-and-adapters/phase-7-composition-root.md
17
17
  */
18
- import { type AppContext, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssertionRegistration, type Logger, type ResolvedConfig } from "./_vendor/ailf-core/index.d.ts";
18
+ import { type AppContext, type ArtifactWriter, type ArtifactWriterProgressOptions, type AssertionRegistration, type LLMClient, type Logger, type ResolvedConfig } from "./_vendor/ailf-core/index.d.ts";
19
19
  import { CompositeTaskSource, ContentLakeTaskSource, RepoTaskSource } from "./adapters/task-sources/index.js";
20
20
  /**
21
21
  * Create a fully wired AppContext from resolved configuration.
@@ -24,6 +24,28 @@ import { CompositeTaskSource, ContentLakeTaskSource, RepoTaskSource } from "./ad
24
24
  * Swapping an adapter is a one-line change in this function.
25
25
  */
26
26
  export declare function createAppContext(config: ResolvedConfig): AppContext;
27
+ /**
28
+ * Typed key bag passed to `createLLMClient`. The composition root reads
29
+ * env once and supplies values here; the factory stays pure so tests don't
30
+ * have to mutate `process.env`.
31
+ */
32
+ export interface LLMClientKeys {
33
+ anthropicApiKey?: string;
34
+ openaiApiKey?: string;
35
+ }
36
+ /**
37
+ * Select the LLMClient adapter based on `config.llmProvider` and the
38
+ * supplied API keys. Returns `undefined` when no usable credential is
39
+ * present — `AppContext.llmClient` stays unset and consumers handle that
40
+ * explicitly.
41
+ *
42
+ * Adapters never read `process.env` themselves (per
43
+ * `.claude/rules/typescript.md`); env mapping happens at the call site
44
+ * (typically `createAppContext`).
45
+ *
46
+ * Exported for unit-test access; not part of the public package API.
47
+ */
48
+ export declare function createLLMClient(config: ResolvedConfig, keys: LLMClientKeys, logger: Logger): LLMClient | undefined;
27
49
  /**
28
50
  * Selects the `ArtifactWriter` wiring per D0033 M4:
29
51
  *
@@ -26,6 +26,7 @@ import { LocalFilesystemArtifactWriter } from "./artifact-capture/local-fs-artif
26
26
  import { resolveUploadConcurrency, setDefaultUploadConcurrency, } from "./artifact-capture/parallel-emit.js";
27
27
  import { UploadMetrics } from "./artifact-capture/upload-metrics.js";
28
28
  import { ContentLakeCacheAdapter } from "./adapters/cache/content-lake-cache.js";
29
+ import { AnthropicLLMClient, OpenAILLMClient } from "./adapters/llm/index.js";
29
30
  import { loadExternalPresets } from "./pipeline/compiler/preset-loader.js";
30
31
  import { FilesystemCache } from "./adapters/cache/filesystem-cache.js";
31
32
  import { PromptfooEvalAdapter } from "./adapters/eval-runners/promptfoo-eval-adapter.js";
@@ -91,12 +92,20 @@ export function createAppContext(config) {
91
92
  // from the context (D0032).
92
93
  const runId = generateRunId();
93
94
  logger.debug(`Pipeline runId: ${runId}`);
95
+ // LLM client (D0051) — wired when an API key is present. The grader path
96
+ // does NOT consume this; D0051 defers grader migration as a follow-up.
97
+ // Env mapping happens here so `createLLMClient` stays pure and testable.
98
+ const llmClient = createLLMClient(config, {
99
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
100
+ openaiApiKey: process.env.OPENAI_API_KEY,
101
+ }, logger);
94
102
  return {
95
103
  artifactWriter,
96
104
  cache,
97
105
  config,
98
106
  docFetcher,
99
107
  evalRunner,
108
+ ...(llmClient ? { llmClient } : {}),
100
109
  logger,
101
110
  packageSurfaceResolver,
102
111
  progress,
@@ -107,6 +116,44 @@ export function createAppContext(config) {
107
116
  taskSource,
108
117
  };
109
118
  }
119
+ /**
120
+ * Select the LLMClient adapter based on `config.llmProvider` and the
121
+ * supplied API keys. Returns `undefined` when no usable credential is
122
+ * present — `AppContext.llmClient` stays unset and consumers handle that
123
+ * explicitly.
124
+ *
125
+ * Adapters never read `process.env` themselves (per
126
+ * `.claude/rules/typescript.md`); env mapping happens at the call site
127
+ * (typically `createAppContext`).
128
+ *
129
+ * Exported for unit-test access; not part of the public package API.
130
+ */
131
+ export function createLLMClient(config, keys, logger) {
132
+ const explicit = config.llmProvider;
133
+ const anthropicKey = keys.anthropicApiKey;
134
+ const openaiKey = keys.openaiApiKey;
135
+ // Auto-select: prefer Anthropic when both are present (matches the
136
+ // current grader's default model in `config/models.ts`).
137
+ const provider = explicit ?? (anthropicKey ? "anthropic" : openaiKey ? "openai" : undefined);
138
+ if (!provider) {
139
+ logger.debug("LLM client: not wired — no Anthropic or OpenAI API key supplied");
140
+ return undefined;
141
+ }
142
+ if (provider === "anthropic") {
143
+ if (!anthropicKey) {
144
+ logger.warn('llmProvider="anthropic" but no Anthropic API key supplied — LLMClient not wired');
145
+ return undefined;
146
+ }
147
+ logger.debug("LLM client: AnthropicLLMClient");
148
+ return new AnthropicLLMClient({ apiKey: anthropicKey, logger });
149
+ }
150
+ if (!openaiKey) {
151
+ logger.warn('llmProvider="openai" but no OpenAI API key supplied — LLMClient not wired');
152
+ return undefined;
153
+ }
154
+ logger.debug("LLM client: OpenAILLMClient");
155
+ return new OpenAILLMClient({ apiKey: openaiKey, logger });
156
+ }
110
157
  // ---------------------------------------------------------------------------
111
158
  // Sub-factories (extracted to keep createAppContext readable)
112
159
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ailf",
3
- "version": "4.4.0",
3
+ "version": "4.6.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -55,8 +55,8 @@
55
55
  "tsx": "^4.19.2",
56
56
  "typescript": "^5.7.3",
57
57
  "vitest": "^4.1.5",
58
- "@sanity/ailf-core": "0.1.0",
59
- "@sanity/ailf-shared": "0.1.0"
58
+ "@sanity/ailf-shared": "0.1.0",
59
+ "@sanity/ailf-core": "0.1.0"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc && tsx scripts/bundle-workspace-deps.ts",