@aplaytest/llm 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAG3F,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAC;AAE5C,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IACzC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;CACzE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,mBAAwB,GAAG,SAAS,CAgB5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAG3F"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Client construction.
3
+ *
4
+ * Resolving to an UnavailableLlmClient rather than throwing at startup is the
5
+ * point: every deterministic feature keeps working, and only the code paths
6
+ * that genuinely need a model fail — with a reason that names itself.
7
+ */
8
+ import { AnthropicLlmClient } from './providers/anthropic.js';
9
+ import { UnavailableLlmClient } from './providers/unavailable.js';
10
+ /**
11
+ * Build an {@link LlmClient} from options and the environment.
12
+ *
13
+ * Resolves to {@link UnavailableLlmClient} rather than throwing: every
14
+ * deterministic feature keeps working, and only paths that genuinely need a
15
+ * model fail — with a reason that names itself.
16
+ *
17
+ * @param options - Provider, API key, and an optional `env` override (tests
18
+ * inject this so they never touch `process.env`).
19
+ * @returns A live Anthropic client, or an unavailable client whose
20
+ * `complete()` rejects with `LlmUnavailableError`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const llm = createLlmClient(); // unavailable when ANTHROPIC_API_KEY is unset
25
+ * if (!llm.available) {
26
+ * // heal, flaky, impact still run
27
+ * }
28
+ * ```
29
+ */
30
+ export function createLlmClient(options = {}) {
31
+ if (options.disabled === true) {
32
+ return new UnavailableLlmClient('disabled by --no-llm');
33
+ }
34
+ if (options.provider === 'none') {
35
+ return new UnavailableLlmClient('provider is set to "none"');
36
+ }
37
+ const env = options.env ?? process.env;
38
+ const apiKey = options.apiKey ?? env['ANTHROPIC_API_KEY'];
39
+ if (apiKey === undefined || apiKey === '') {
40
+ return new UnavailableLlmClient('ANTHROPIC_API_KEY is not set');
41
+ }
42
+ return new AnthropicLlmClient({ ...options, apiKey });
43
+ }
44
+ /**
45
+ * One-line description of which roles a client can serve, for `aplaytest doctor`
46
+ * and CLI banners.
47
+ *
48
+ * @param client - The client to describe.
49
+ * @param roles - Roles to include in the description.
50
+ * @returns `"no model — deterministic tier only"` when unavailable, otherwise
51
+ * a `role: model-id` list.
52
+ */
53
+ export function describeAvailability(client, roles) {
54
+ if (!client.available)
55
+ return 'no model — deterministic tier only';
56
+ return roles.map(role => `${role}: ${client.modelFor(role)}`).join(' · ');
57
+ }
58
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,kBAAkB,EAA+B,MAAM,0BAA0B,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAWlE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe,CAAC,UAA+B,EAAE;IAC/D,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,IAAI,oBAAoB,CAAC,sBAAsB,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAChC,OAAO,IAAI,oBAAoB,CAAC,2BAA2B,CAAC,CAAC;IAC/D,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAE1D,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAC1C,OAAO,IAAI,oBAAoB,CAAC,8BAA8B,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,IAAI,kBAAkB,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAiB,EAAE,KAA2B;IACjF,IAAI,CAAC,MAAM,CAAC,SAAS;QAAE,OAAO,oCAAoC,CAAC;IACnE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC"}
package/dist/fake.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A scripted client for tests.
3
+ *
4
+ * Exported from the package, not hidden in a test folder, because the agent
5
+ * runtime in @aplaytest/agent needs it too — and because an agent loop that can
6
+ * only be exercised with a live API key is an agent loop nobody will test.
7
+ */
8
+ import type { z } from 'zod';
9
+ import { type CompleteRequest, type CompleteResponse, type LlmClient, type ModelRole, type StructuredResponse, type Usage } from './client.js';
10
+ export interface ScriptedTurn {
11
+ /** Text the model "returns". Objects are serialised. */
12
+ readonly reply: string | object;
13
+ readonly refused?: boolean;
14
+ readonly refusalCategory?: string;
15
+ readonly usage?: Partial<Usage>;
16
+ }
17
+ export declare class FakeLlmClient implements LlmClient {
18
+ private readonly script;
19
+ readonly available = true;
20
+ /** Every request made, in order — assert against these. */
21
+ readonly requests: CompleteRequest[];
22
+ private turn;
23
+ constructor(script: readonly ScriptedTurn[]);
24
+ modelFor(role: ModelRole): string;
25
+ get callCount(): number;
26
+ complete(request: CompleteRequest): Promise<CompleteResponse>;
27
+ completeStructured<T>(request: CompleteRequest, schema: z.ZodType<T>): Promise<StructuredResponse<T>>;
28
+ }
29
+ //# sourceMappingURL=fake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake.d.ts","sourceRoot":"","sources":["../src/fake.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACX,MAAM,aAAa,CAAC;AAIrB,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;CACjC;AAED,qBAAa,aAAc,YAAW,SAAS;IAMjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IALnC,QAAQ,CAAC,SAAS,QAAQ;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAM;IAC1C,OAAO,CAAC,IAAI,CAAK;gBAEY,MAAM,EAAE,SAAS,YAAY,EAAE;IAE5D,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM;IAIjC,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsB7D,kBAAkB,CAAC,CAAC,EACxB,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;CAmBlC"}
package/dist/fake.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A scripted client for tests.
3
+ *
4
+ * Exported from the package, not hidden in a test folder, because the agent
5
+ * runtime in @aplaytest/agent needs it too — and because an agent loop that can
6
+ * only be exercised with a live API key is an agent loop nobody will test.
7
+ */
8
+ import { EMPTY_USAGE, } from './client.js';
9
+ import { RefusalError, StructuredOutputError } from './errors.js';
10
+ import { DEFAULT_MODELS } from './pricing.js';
11
+ export class FakeLlmClient {
12
+ script;
13
+ available = true;
14
+ /** Every request made, in order — assert against these. */
15
+ requests = [];
16
+ turn = 0;
17
+ constructor(script) {
18
+ this.script = script;
19
+ }
20
+ modelFor(role) {
21
+ return DEFAULT_MODELS[role];
22
+ }
23
+ get callCount() {
24
+ return this.turn;
25
+ }
26
+ async complete(request) {
27
+ this.requests.push(request);
28
+ const scripted = this.script[this.turn];
29
+ this.turn += 1;
30
+ if (scripted === undefined) {
31
+ throw new Error(`FakeLlmClient ran out of script at call ${this.turn} — the code under test made ` +
32
+ 'more model calls than expected.');
33
+ }
34
+ const text = typeof scripted.reply === 'string' ? scripted.reply : JSON.stringify(scripted.reply);
35
+ return {
36
+ text: scripted.refused === true ? '' : text,
37
+ usage: { ...EMPTY_USAGE, outputTokens: text.length, ...scripted.usage },
38
+ model: this.modelFor(request.role),
39
+ refused: scripted.refused === true,
40
+ refusalCategory: scripted.refusalCategory ?? null,
41
+ };
42
+ }
43
+ async completeStructured(request, schema) {
44
+ const first = await this.complete(request);
45
+ if (first.refused)
46
+ throw new RefusalError(first.refusalCategory);
47
+ const parsed = schema.safeParse(safeJson(first.text));
48
+ if (parsed.success)
49
+ return { ...first, value: parsed.data, raw: first.text, repaired: false };
50
+ const retry = await this.complete(request);
51
+ if (retry.refused)
52
+ throw new RefusalError(retry.refusalCategory);
53
+ const second = schema.safeParse(safeJson(retry.text));
54
+ if (!second.success) {
55
+ throw new StructuredOutputError(second.error.issues.map(i => i.message).join(', '), retry.text);
56
+ }
57
+ return { ...retry, value: second.data, raw: retry.text, repaired: true };
58
+ }
59
+ }
60
+ function safeJson(text) {
61
+ try {
62
+ return JSON.parse(text);
63
+ }
64
+ catch {
65
+ return text;
66
+ }
67
+ }
68
+ //# sourceMappingURL=fake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake.js","sourceRoot":"","sources":["../src/fake.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @aplaytest/llm — the only package that talks to a model.
3
+ *
4
+ * Everything else takes an injected `LlmClient`, which is what keeps a model
5
+ * SDK out of the reporter (running in every test worker) and lets the agent
6
+ * loop be tested against a scripted fake.
7
+ */
8
+ export type { LlmClient, ModelRole, Effort, Message, Usage, CompleteRequest, CompleteResponse, StructuredResponse, } from './client.js';
9
+ export { EMPTY_USAGE } from './client.js';
10
+ export { createLlmClient, describeAvailability } from './factory.js';
11
+ export type { CreateClientOptions, Provider } from './factory.js';
12
+ export { AnthropicLlmClient } from './providers/anthropic.js';
13
+ export { UnavailableLlmClient } from './providers/unavailable.js';
14
+ export { FakeLlmClient } from './fake.js';
15
+ export type { ScriptedTurn } from './fake.js';
16
+ export { BudgetGuard, DEFAULT_BUDGET } from './budget.js';
17
+ export type { BudgetLimits, BudgetState } from './budget.js';
18
+ export { MODELS, DEFAULT_MODELS, DEFAULT_EFFORT, specFor, costUsd, checkCacheable, estimateTokens, } from './pricing.js';
19
+ export type { ModelSpec, TokenCounts, CacheWarning } from './pricing.js';
20
+ export { LlmUnavailableError, BudgetExceededError, RefusalError, StructuredOutputError, } from './errors.js';
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,SAAS,EACT,SAAS,EACT,MAAM,EACN,OAAO,EACP,KAAK,EACL,eAAe,EACf,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAElE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EACL,MAAM,EACN,cAAc,EACd,cAAc,EACd,OAAO,EACP,OAAO,EACP,cAAc,EACd,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @aplaytest/llm — the only package that talks to a model.
3
+ *
4
+ * Everything else takes an injected `LlmClient`, which is what keeps a model
5
+ * SDK out of the reporter (running in every test worker) and lets the agent
6
+ * loop be tested against a scripted fake.
7
+ */
8
+ export { EMPTY_USAGE } from './client.js';
9
+ export { createLlmClient, describeAvailability } from './factory.js';
10
+ export { AnthropicLlmClient } from './providers/anthropic.js';
11
+ export { UnavailableLlmClient } from './providers/unavailable.js';
12
+ export { FakeLlmClient } from './fake.js';
13
+ export { BudgetGuard, DEFAULT_BUDGET } from './budget.js';
14
+ export { MODELS, DEFAULT_MODELS, DEFAULT_EFFORT, specFor, costUsd, checkCacheable, estimateTokens, } from './pricing.js';
15
+ export { LlmUnavailableError, BudgetExceededError, RefusalError, StructuredOutputError, } from './errors.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG1D,OAAO,EACL,MAAM,EACN,cAAc,EACd,cAAc,EACd,OAAO,EACP,OAAO,EACP,cAAc,EACd,cAAc,GACf,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,YAAY,EACZ,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Model selection, pricing, and the cache-prefix rules.
3
+ *
4
+ * Cost is computed from real per-token rates rather than estimated, so the
5
+ * CLI can always disclose what a run actually spent. A user must never have
6
+ * to wonder whether a number came from a model or a measurement.
7
+ */
8
+ import type { Effort, ModelRole } from './client.js';
9
+ export interface ModelSpec {
10
+ readonly id: string;
11
+ /** USD per million input tokens. */
12
+ readonly inputPerMTok: number;
13
+ readonly outputPerMTok: number;
14
+ /**
15
+ * Minimum cacheable prefix, in tokens.
16
+ *
17
+ * NOT monotonic across generations, which is the trap: 512 on Opus 5 but
18
+ * 4096 on Haiku 4.5. A conventions block that caches fine on the heal path
19
+ * can silently fail to cache on the classify path, with no error — just
20
+ * `cache_creation_input_tokens: 0` forever.
21
+ */
22
+ readonly minCacheablePrefix: number;
23
+ readonly contextWindow: number;
24
+ }
25
+ export declare const MODELS: Readonly<Record<string, ModelSpec>>;
26
+ /**
27
+ * Role → model. Split by JOB SHAPE, not by a vague quality axis.
28
+ *
29
+ * classify — taxonomy tie-breaks and phrasing. High volume, low stakes, and
30
+ * the deterministic tier has already done the real work.
31
+ * heal — ranking pre-verified candidates by intent. A judgement call
32
+ * between a handful of options, not a research task.
33
+ * author — planning, browser exploration, code synthesis.
34
+ */
35
+ export declare const DEFAULT_MODELS: Readonly<Record<ModelRole, string>>;
36
+ export declare const DEFAULT_EFFORT: Readonly<Record<ModelRole, Effort>>;
37
+ export declare function specFor(modelId: string): ModelSpec | null;
38
+ export interface TokenCounts {
39
+ readonly inputTokens: number;
40
+ readonly outputTokens: number;
41
+ readonly cacheReadTokens: number;
42
+ readonly cacheWriteTokens: number;
43
+ }
44
+ export declare function costUsd(modelId: string, counts: TokenCounts): number;
45
+ export interface CacheWarning {
46
+ readonly model: string;
47
+ readonly prefixTokens: number;
48
+ readonly minimum: number;
49
+ readonly message: string;
50
+ }
51
+ /** Rough token estimate. Only used to warn, never to bill. */
52
+ export declare function estimateTokens(text: string): number;
53
+ /**
54
+ * Warn when a prefix is too short to cache on this model.
55
+ *
56
+ * Silent is the problem: below the minimum the API simply does not cache and
57
+ * reports nothing, so the first sign is a bill.
58
+ */
59
+ export declare function checkCacheable(modelId: string, systemPrompt: string): CacheWarning | null;
60
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAErD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;;;;;;OAOG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,eAAO,MAAM,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAsBtD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAK9D,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAM9D,CAAC;AAEF,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAEzD;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAUD,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAapE;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAgBzF"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Model selection, pricing, and the cache-prefix rules.
3
+ *
4
+ * Cost is computed from real per-token rates rather than estimated, so the
5
+ * CLI can always disclose what a run actually spent. A user must never have
6
+ * to wonder whether a number came from a model or a measurement.
7
+ */
8
+ export const MODELS = {
9
+ 'claude-opus-5': {
10
+ id: 'claude-opus-5',
11
+ inputPerMTok: 5,
12
+ outputPerMTok: 25,
13
+ minCacheablePrefix: 512,
14
+ contextWindow: 1_000_000,
15
+ },
16
+ 'claude-sonnet-5': {
17
+ id: 'claude-sonnet-5',
18
+ inputPerMTok: 3,
19
+ outputPerMTok: 15,
20
+ minCacheablePrefix: 1024,
21
+ contextWindow: 1_000_000,
22
+ },
23
+ 'claude-haiku-4-5': {
24
+ id: 'claude-haiku-4-5',
25
+ inputPerMTok: 1,
26
+ outputPerMTok: 5,
27
+ minCacheablePrefix: 4096,
28
+ contextWindow: 200_000,
29
+ },
30
+ };
31
+ /**
32
+ * Role → model. Split by JOB SHAPE, not by a vague quality axis.
33
+ *
34
+ * classify — taxonomy tie-breaks and phrasing. High volume, low stakes, and
35
+ * the deterministic tier has already done the real work.
36
+ * heal — ranking pre-verified candidates by intent. A judgement call
37
+ * between a handful of options, not a research task.
38
+ * author — planning, browser exploration, code synthesis.
39
+ */
40
+ export const DEFAULT_MODELS = {
41
+ classify: 'claude-haiku-4-5',
42
+ heal: 'claude-sonnet-5',
43
+ author: 'claude-opus-5',
44
+ vision: 'claude-sonnet-5',
45
+ };
46
+ export const DEFAULT_EFFORT = {
47
+ classify: 'low',
48
+ heal: 'medium',
49
+ // The documented setting for coding and agentic work.
50
+ author: 'xhigh',
51
+ vision: 'medium',
52
+ };
53
+ export function specFor(modelId) {
54
+ return MODELS[modelId] ?? null;
55
+ }
56
+ /**
57
+ * Cache reads cost ~0.1× base input; writes cost 1.25× (5-minute TTL).
58
+ * Two requests break even, so an analyze pass over several failures is well
59
+ * past the threshold on its second call.
60
+ */
61
+ const CACHE_READ_MULTIPLIER = 0.1;
62
+ const CACHE_WRITE_MULTIPLIER = 1.25;
63
+ export function costUsd(modelId, counts) {
64
+ const spec = specFor(modelId);
65
+ if (spec === null)
66
+ return 0;
67
+ const perInputToken = spec.inputPerMTok / 1_000_000;
68
+ const perOutputToken = spec.outputPerMTok / 1_000_000;
69
+ return (counts.inputTokens * perInputToken +
70
+ counts.outputTokens * perOutputToken +
71
+ counts.cacheReadTokens * perInputToken * CACHE_READ_MULTIPLIER +
72
+ counts.cacheWriteTokens * perInputToken * CACHE_WRITE_MULTIPLIER);
73
+ }
74
+ /** Rough token estimate. Only used to warn, never to bill. */
75
+ export function estimateTokens(text) {
76
+ return Math.ceil(text.length / 4);
77
+ }
78
+ /**
79
+ * Warn when a prefix is too short to cache on this model.
80
+ *
81
+ * Silent is the problem: below the minimum the API simply does not cache and
82
+ * reports nothing, so the first sign is a bill.
83
+ */
84
+ export function checkCacheable(modelId, systemPrompt) {
85
+ const spec = specFor(modelId);
86
+ if (spec === null)
87
+ return null;
88
+ const prefixTokens = estimateTokens(systemPrompt);
89
+ if (prefixTokens >= spec.minCacheablePrefix)
90
+ return null;
91
+ return {
92
+ model: modelId,
93
+ prefixTokens,
94
+ minimum: spec.minCacheablePrefix,
95
+ message: `Cache prefix is ~${prefixTokens} tokens, below the ${spec.minCacheablePrefix}-token ` +
96
+ `minimum for ${modelId}. cache_control will be silently ignored — no error, just no ` +
97
+ 'caching. Note the minimum is not monotonic across models.',
98
+ };
99
+ }
100
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH,MAAM,CAAC,MAAM,MAAM,GAAwC;IACzD,eAAe,EAAE;QACf,EAAE,EAAE,eAAe;QACnB,YAAY,EAAE,CAAC;QACf,aAAa,EAAE,EAAE;QACjB,kBAAkB,EAAE,GAAG;QACvB,aAAa,EAAE,SAAS;KACzB;IACD,iBAAiB,EAAE;QACjB,EAAE,EAAE,iBAAiB;QACrB,YAAY,EAAE,CAAC;QACf,aAAa,EAAE,EAAE;QACjB,kBAAkB,EAAE,IAAI;QACxB,aAAa,EAAE,SAAS;KACzB;IACD,kBAAkB,EAAE;QAClB,EAAE,EAAE,kBAAkB;QACtB,YAAY,EAAE,CAAC;QACf,aAAa,EAAE,CAAC;QAChB,kBAAkB,EAAE,IAAI;QACxB,aAAa,EAAE,OAAO;KACvB;CACF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GAAwC;IACjE,QAAQ,EAAE,kBAAkB;IAC5B,IAAI,EAAE,iBAAiB;IACvB,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,iBAAiB;CAC1B,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAwC;IACjE,QAAQ,EAAE,KAAK;IACf,IAAI,EAAE,QAAQ;IACd,sDAAsD;IACtD,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,QAAQ;CACjB,CAAC;AAEF,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;AACjC,CAAC;AASD;;;;GAIG;AACH,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAEpC,MAAM,UAAU,OAAO,CAAC,OAAe,EAAE,MAAmB;IAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC;IAE5B,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;IACpD,MAAM,cAAc,GAAG,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;IAEtD,OAAO,CACL,MAAM,CAAC,WAAW,GAAG,aAAa;QAClC,MAAM,CAAC,YAAY,GAAG,cAAc;QACpC,MAAM,CAAC,eAAe,GAAG,aAAa,GAAG,qBAAqB;QAC9D,MAAM,CAAC,gBAAgB,GAAG,aAAa,GAAG,sBAAsB,CACjE,CAAC;AACJ,CAAC;AASD,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,YAAoB;IAClE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE/B,MAAM,YAAY,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;IAClD,IAAI,YAAY,IAAI,IAAI,CAAC,kBAAkB;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO;QACL,KAAK,EAAE,OAAO;QACd,YAAY;QACZ,OAAO,EAAE,IAAI,CAAC,kBAAkB;QAChC,OAAO,EACL,oBAAoB,YAAY,sBAAsB,IAAI,CAAC,kBAAkB,SAAS;YACtF,eAAe,OAAO,+DAA+D;YACrF,2DAA2D;KAC9D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The Anthropic provider.
3
+ *
4
+ * Three things here are deliberate and easy to get wrong:
5
+ *
6
+ * 1. NO sampling parameters. `temperature`, `top_p` and `top_k` are rejected
7
+ * with a 400 on current Opus and Sonnet models. Depth is `effort`.
8
+ * 2. The system prompt is a cacheable block, ordered first. It is the only
9
+ * part guaranteed byte-identical across a run, so it is the cache prefix.
10
+ * 3. `stop_reason: "refusal"` arrives as a normal HTTP 200. Reading
11
+ * `content[0]` without checking it will break.
12
+ */
13
+ import type { z } from 'zod';
14
+ import { EMPTY_USAGE, type CompleteRequest, type CompleteResponse, type LlmClient, type ModelRole, type StructuredResponse, type Usage } from '../client.js';
15
+ export interface AnthropicClientOptions {
16
+ readonly apiKey?: string | undefined;
17
+ readonly models?: Partial<Record<ModelRole, string>> | undefined;
18
+ readonly maxTokens?: number | undefined;
19
+ /** Emitted for each call so the CLI can disclose model usage inline. */
20
+ onUsage?: ((usage: Usage, model: string) => void) | undefined;
21
+ }
22
+ export declare class AnthropicLlmClient implements LlmClient {
23
+ private readonly options;
24
+ readonly available = true;
25
+ private readonly anthropic;
26
+ private readonly models;
27
+ constructor(options?: AnthropicClientOptions);
28
+ modelFor(role: ModelRole): string;
29
+ complete(request: CompleteRequest): Promise<CompleteResponse>;
30
+ /**
31
+ * Structured output via the API's own schema constraint, not prompting.
32
+ *
33
+ * The earlier implementation asked for JSON in prose and re-parsed the text,
34
+ * with one repair round when that failed. Measured against the live model,
35
+ * that is not a robustness question — it is a correctness one. The author
36
+ * agent's prompt asked for a test plan and never mentioned JSON, so the model
37
+ * returned an excellent Markdown plan; every field parsed as `undefined`, the
38
+ * repair round produced more prose, and the run failed after 100 seconds and
39
+ * $0.19. The repair agent only worked because its prompt happened to carry
40
+ * "Reply with JSON only, matching this shape:".
41
+ *
42
+ * `output_config.format` constrains the response at the API level, so the
43
+ * schema is enforced rather than requested and the repair round has nothing
44
+ * left to do.
45
+ */
46
+ completeStructured<T>(request: CompleteRequest, schema: z.ZodType<T>): Promise<StructuredResponse<T>>;
47
+ private toUsage;
48
+ }
49
+ export declare function mergeUsage(a: Usage, b: Usage): Usage;
50
+ export { EMPTY_USAGE };
51
+ //# sourceMappingURL=anthropic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EACL,WAAW,EACX,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,KAAK,EACX,MAAM,cAAc,CAAC;AAItB,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CAC/D;AAyBD,qBAAa,kBAAmB,YAAW,SAAS;IAKtC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,QAAQ,CAAC,SAAS,QAAQ;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4B;gBAEtB,OAAO,GAAE,sBAA2B;IAMjE,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM;IAI3B,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoCnE;;;;;;;;;;;;;;;OAeG;IACG,kBAAkB,CAAC,CAAC,EACxB,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAsDjC,OAAO,CAAC,OAAO;CAShB;AAGD,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAQpD;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The Anthropic provider.
3
+ *
4
+ * Three things here are deliberate and easy to get wrong:
5
+ *
6
+ * 1. NO sampling parameters. `temperature`, `top_p` and `top_k` are rejected
7
+ * with a 400 on current Opus and Sonnet models. Depth is `effort`.
8
+ * 2. The system prompt is a cacheable block, ordered first. It is the only
9
+ * part guaranteed byte-identical across a run, so it is the cache prefix.
10
+ * 3. `stop_reason: "refusal"` arrives as a normal HTTP 200. Reading
11
+ * `content[0]` without checking it will break.
12
+ */
13
+ import Anthropic from '@anthropic-ai/sdk';
14
+ import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
15
+ import { EMPTY_USAGE, } from '../client.js';
16
+ import { RefusalError, StructuredOutputError } from '../errors.js';
17
+ import { DEFAULT_EFFORT, DEFAULT_MODELS, costUsd } from '../pricing.js';
18
+ const DEFAULT_MAX_TOKENS = 16_000;
19
+ function extractText(response) {
20
+ return response.content
21
+ .filter(block => block.type === 'text')
22
+ .map(block => block.text ?? '')
23
+ .join('');
24
+ }
25
+ export class AnthropicLlmClient {
26
+ options;
27
+ available = true;
28
+ anthropic;
29
+ models;
30
+ constructor(options = {}) {
31
+ this.options = options;
32
+ this.anthropic =
33
+ options.apiKey === undefined ? new Anthropic() : new Anthropic({ apiKey: options.apiKey });
34
+ this.models = { ...DEFAULT_MODELS, ...options.models };
35
+ }
36
+ modelFor(role) {
37
+ return this.models[role];
38
+ }
39
+ async complete(request) {
40
+ const model = this.modelFor(request.role);
41
+ const response = (await this.anthropic.messages.create({
42
+ model,
43
+ max_tokens: request.maxTokens ?? this.options.maxTokens ?? DEFAULT_MAX_TOKENS,
44
+ // The cache prefix: marked cacheable, and rendered before `messages`.
45
+ system: [
46
+ {
47
+ type: 'text',
48
+ text: request.system,
49
+ cache_control: { type: 'ephemeral' },
50
+ },
51
+ ],
52
+ messages: request.messages.map(m => ({ role: m.role, content: m.content })),
53
+ // Depth, not sampling. `temperature` here would be a 400.
54
+ output_config: { effort: request.effort ?? DEFAULT_EFFORT[request.role] },
55
+ thinking: { type: 'adaptive' },
56
+ }));
57
+ const usage = this.toUsage(response, model);
58
+ this.options.onUsage?.(usage, model);
59
+ const refused = response.stop_reason === 'refusal';
60
+ return {
61
+ // Empty on refusal rather than whatever partial text exists: callers
62
+ // that ignore `refused` should get nothing, not something plausible.
63
+ text: refused ? '' : extractText(response),
64
+ usage,
65
+ model: response.model ?? model,
66
+ refused,
67
+ refusalCategory: response.stop_details?.category ?? null,
68
+ };
69
+ }
70
+ /**
71
+ * Structured output via the API's own schema constraint, not prompting.
72
+ *
73
+ * The earlier implementation asked for JSON in prose and re-parsed the text,
74
+ * with one repair round when that failed. Measured against the live model,
75
+ * that is not a robustness question — it is a correctness one. The author
76
+ * agent's prompt asked for a test plan and never mentioned JSON, so the model
77
+ * returned an excellent Markdown plan; every field parsed as `undefined`, the
78
+ * repair round produced more prose, and the run failed after 100 seconds and
79
+ * $0.19. The repair agent only worked because its prompt happened to carry
80
+ * "Reply with JSON only, matching this shape:".
81
+ *
82
+ * `output_config.format` constrains the response at the API level, so the
83
+ * schema is enforced rather than requested and the repair round has nothing
84
+ * left to do.
85
+ */
86
+ async completeStructured(request, schema) {
87
+ const model = this.modelFor(request.role);
88
+ const response = (await this.anthropic.messages.parse({
89
+ model,
90
+ max_tokens: request.maxTokens ?? this.options.maxTokens ?? DEFAULT_MAX_TOKENS,
91
+ system: [
92
+ {
93
+ type: 'text',
94
+ text: request.system,
95
+ cache_control: { type: 'ephemeral' },
96
+ },
97
+ ],
98
+ messages: request.messages.map(m => ({ role: m.role, content: m.content })),
99
+ output_config: {
100
+ effort: request.effort ?? DEFAULT_EFFORT[request.role],
101
+ format: zodOutputFormat(schema),
102
+ },
103
+ thinking: { type: 'adaptive' },
104
+ }));
105
+ const usage = this.toUsage(response, model);
106
+ this.options.onUsage?.(usage, model);
107
+ if (response.stop_reason === 'refusal') {
108
+ throw new RefusalError(response.stop_details?.category ?? null);
109
+ }
110
+ const text = extractText(response);
111
+ if (response.parsed_output === null || response.parsed_output === undefined) {
112
+ // Reached when the response is schema-shaped but fails a constraint the
113
+ // API does not enforce (string lengths, array bounds — the SDK strips
114
+ // those from the wire schema and checks them client-side), or when the
115
+ // turn was truncated. Nothing a retry would fix, so it fails loudly.
116
+ throw new StructuredOutputError(response.stop_reason === 'max_tokens'
117
+ ? 'response hit max_tokens before the schema was satisfied'
118
+ : 'response did not satisfy the schema', text);
119
+ }
120
+ return {
121
+ usage,
122
+ model: response.model ?? model,
123
+ refused: false,
124
+ refusalCategory: null,
125
+ value: response.parsed_output,
126
+ raw: text,
127
+ repaired: false,
128
+ };
129
+ }
130
+ toUsage(response, model) {
131
+ const counts = {
132
+ inputTokens: response.usage?.input_tokens ?? 0,
133
+ outputTokens: response.usage?.output_tokens ?? 0,
134
+ cacheReadTokens: response.usage?.cache_read_input_tokens ?? 0,
135
+ cacheWriteTokens: response.usage?.cache_creation_input_tokens ?? 0,
136
+ };
137
+ return { ...counts, usd: costUsd(model, counts) };
138
+ }
139
+ }
140
+ export function mergeUsage(a, b) {
141
+ return {
142
+ inputTokens: a.inputTokens + b.inputTokens,
143
+ outputTokens: a.outputTokens + b.outputTokens,
144
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
145
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
146
+ usd: a.usd + b.usd,
147
+ };
148
+ }
149
+ export { EMPTY_USAGE };
150
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The no-model client.
3
+ *
4
+ * Every call throws a NAMED error rather than returning empty output. That is
5
+ * the degradation contract: a command either works, works with reduced scope,
6
+ * or exits non-zero saying which. What it must never do is quietly produce a
7
+ * worse answer that looks like a normal one.
8
+ */
9
+ import type { z } from 'zod';
10
+ import type { CompleteRequest, CompleteResponse, LlmClient, ModelRole, StructuredResponse } from '../client.js';
11
+ export declare class UnavailableLlmClient implements LlmClient {
12
+ private readonly reason;
13
+ readonly available = false;
14
+ constructor(reason: string);
15
+ modelFor(role: ModelRole): string;
16
+ complete(_request: CompleteRequest): Promise<CompleteResponse>;
17
+ completeStructured<T>(_request: CompleteRequest, _schema: z.ZodType<T>): Promise<StructuredResponse<T>>;
18
+ }
19
+ //# sourceMappingURL=unavailable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unavailable.d.ts","sourceRoot":"","sources":["../../src/providers/unavailable.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAItB,qBAAa,oBAAqB,YAAW,SAAS;IAGxC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,SAAS,SAAS;gBAEE,MAAM,EAAE,MAAM;IAE3C,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM;IAI3B,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAI9D,kBAAkB,CAAC,CAAC,EACxB,QAAQ,EAAE,eAAe,EACzB,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;CAGlC"}