@connextar/house 0.8.1 → 0.9.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.
package/README.md CHANGED
@@ -634,6 +634,89 @@ ten results and two for eleven to a hundred; VALUE SERP charges per request.
634
634
  There is no caching here — what may be stored, and for how long, is each app's
635
635
  decision and each provider's terms.
636
636
 
637
+ ### `@connextar/house/ai`
638
+
639
+ One harness for **Anthropic and OpenAI**, so which company answers is a
640
+ deployment setting and never a feature's decision. Extracted from AltEd and
641
+ PharmaLine, which had each written it — and Quickie was about to write it a
642
+ third time. It is the union of the two: AltEd's call layer (prose, structured
643
+ output from a Zod schema, PDFs and pictures, time limits, cache-aware usage) and
644
+ the configuration rules both apps had arrived at independently.
645
+
646
+ The app names its jobs; each job has a model per provider, read from a variable
647
+ the operator sets, with a working default:
648
+
649
+ ```ts
650
+ // lib/ai/index.ts — the app's binding, server-only because it reads keys
651
+ import "server-only";
652
+ import { createAiHarness } from "@connextar/house/ai";
653
+
654
+ export const ai = createAiHarness({
655
+ tasks: {
656
+ categorise: {
657
+ ANTHROPIC: { modelVar: "ANTHROPIC_MODEL", defaultModel: "claude-haiku-4-5-20251001" },
658
+ OPENAI: { modelVar: "OPENAI_MODEL", defaultModel: "gpt-5-mini" },
659
+ },
660
+ insights: {
661
+ ANTHROPIC: { modelVar: "ANTHROPIC_INSIGHTS_MODEL", defaultModel: "claude-sonnet-5" },
662
+ OPENAI: { modelVar: "OPENAI_INSIGHTS_MODEL", defaultModel: "gpt-5" },
663
+ thinking: true,
664
+ },
665
+ },
666
+ });
667
+
668
+ const { parsed, provider, model, inputTokens, outputTokens } = await ai.generateStructured({
669
+ task: "categorise",
670
+ system: "…",
671
+ prompt: "…",
672
+ schema: answerSchema, // a Zod schema
673
+ schemaName: "categories",
674
+ maxTokens: 2000,
675
+ timeoutMs: 25_000,
676
+ });
677
+ ```
678
+
679
+ `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` turn a provider on. `AI_PROVIDER`
680
+ (`anthropic` or `openai`) chooses when both are set; unset, Anthropic is
681
+ preferred — a stable order, so adding a second key never moves an instance to a
682
+ different processor.
683
+
684
+ Rules the module holds so no app has to rediscover them:
685
+
686
+ - **Naming a provider is a decision.** `AI_PROVIDER=openai` with no OpenAI key
687
+ turns the feature _off_ with a reason (`status().problem`); it never falls
688
+ back to the other company, which would break a privacy policy that names its
689
+ processor. Nothing configured at all is simply off, not a problem.
690
+ - **A person is told less than an operator.** `publicStatus()` drops `problem`,
691
+ which names environment variables. Neither status ever carries a key.
692
+ - **Thinking belongs to the job.** Adaptive thinking is sent to Anthropic only
693
+ for a job declared `thinking: true` (or a call that asks) — the small fast
694
+ models reject it with a 400, and the job's default model is where that is
695
+ known. Effort is Anthropic-only; OpenAI's reasoning is the model's own.
696
+ - **OpenAI output budgets get a floor** (`OPENAI_MIN_OUTPUT_TOKENS`, 4,000): a
697
+ reasoning model spends the budget thinking before it writes a word.
698
+ - **Structured calls stream**, because both SDKs refuse a non-streaming call that
699
+ could outlive their timeout. A `timeoutMs` call makes one attempt, no retries.
700
+ - **Failures are three classes.** `AiUnavailableError` — off, nothing was sent.
701
+ `AiServiceError` — the provider said no, in its own words, with its status.
702
+ `AiUnusableAnswerError` — a refusal, a cut-off or the wrong shape; the tokens
703
+ were spent anyway, so `error.usage` carries them for the caller to record.
704
+ - **Usage is cache-aware.** Every result carries input, output, cache-read and
705
+ cache-write tokens. OpenAI counts cached tokens inside `input_tokens`; they are
706
+ moved out, not added, so nothing is billed twice.
707
+
708
+ Pricing is arithmetic only — `costUsd(tokens, rates)`, `modelFamily` (a dated
709
+ snapshot is priced at its family's row) and `findPrice(list, provider, model)`
710
+ over `DEFAULT_PRICES`, the union of both apps' seeded tables with the date each
711
+ rate was checked. **Where rates are kept is the app's decision**: AltEd and
712
+ PharmaLine keep a dated table an operator can correct; a smaller app can price
713
+ straight from the defaults. An unknown model is unpriced (`null`), never free.
714
+
715
+ `@anthropic-ai/sdk`, `openai` and `zod` are optional peer dependencies — only an
716
+ app that imports `/ai` needs them. The SDKs refuse to run in a browser-like
717
+ environment, so a Vitest suite that calls the harness needs
718
+ `// @vitest-environment node` on the file.
719
+
637
720
  ### Styles — `@connextar/house/house.css`
638
721
 
639
722
  What the components need that utility classes cannot say: rich text, the editor's
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Which model provider an instance uses, and which model it uses for which job.
3
+ *
4
+ * Extracted from AltEd and PharmaLine, which had each written this file and were
5
+ * about to be joined by a third. Two rules shape it, and both are why it is
6
+ * shared rather than copied.
7
+ *
8
+ * **Naming a provider is a decision, not a preference.** If `AI_PROVIDER` names
9
+ * a provider whose key is missing, the feature is *off* and says why — it does
10
+ * not quietly send a person's records to the other one. Silently switching
11
+ * processors would break the promise a privacy policy makes by name.
12
+ *
13
+ * **Jobs, not one model.** Sorting a bank line wants a small fast model; writing
14
+ * a report wants a capable one. The app names its jobs and, for each, the
15
+ * environment variable an operator sets per provider and a working default.
16
+ * The model name is provider-specific, so pointing `AI_PROVIDER` at OpenAI can
17
+ * never send a Claude model name to OpenAI.
18
+ *
19
+ * Deliberately free of `server-only` and of anything Next-specific: PharmaLine
20
+ * reads it from a plain Node service. An app binds it from a server-only
21
+ * module of its own, because the provider half reads API keys.
22
+ */
23
+ export declare const AI_PROVIDERS: readonly ["ANTHROPIC", "OPENAI"];
24
+ export type AiProviderName = (typeof AI_PROVIDERS)[number];
25
+ /** How each provider is named to a person — in a privacy policy, in settings, in logs. */
26
+ export declare const PROVIDER_LABELS: Record<AiProviderName, string>;
27
+ /** The environment variable holding each provider's key. */
28
+ export declare const PROVIDER_KEY_VARS: Record<AiProviderName, string>;
29
+ /** One provider's model for one job: the variable an operator sets, and the default when they don't. */
30
+ export interface AiModelSpec {
31
+ modelVar: string;
32
+ /** A working starting point. Every one is overridable; none is load-bearing. */
33
+ defaultModel: string;
34
+ }
35
+ /** One job the app gives a model. */
36
+ export interface AiTaskSpec {
37
+ ANTHROPIC: AiModelSpec;
38
+ OPENAI: AiModelSpec;
39
+ /**
40
+ * Whether structured calls for this job ask Anthropic for adaptive thinking.
41
+ * Off unless the job says so: the small fast models (Haiku 4.5) reject the
42
+ * setting outright with a 400, and a job's default model is exactly where
43
+ * that knowledge belongs. A call can still override it.
44
+ */
45
+ thinking?: boolean;
46
+ }
47
+ /** The app's jobs, by name. */
48
+ export type AiTaskTable<Task extends string> = Record<Task, AiTaskSpec>;
49
+ /** Just the parts of the environment this module reads — so tests need no globals. */
50
+ export type AiEnv = Record<string, string | undefined>;
51
+ /** Providers with a key set, in preference order. */
52
+ export declare function configuredProviders(env: AiEnv): AiProviderName[];
53
+ export interface ProviderChoice {
54
+ provider: AiProviderName | null;
55
+ apiKey: string;
56
+ /** Set when the configuration is contradictory, for an operator to read. */
57
+ problem: string | null;
58
+ }
59
+ /**
60
+ * The provider this instance will use.
61
+ *
62
+ * `AI_PROVIDER` unset means "whichever is configured", preferring Anthropic when
63
+ * both are — an arbitrary but stable order, so adding a second key never silently
64
+ * moves an instance to a different processor. Nothing configured at all is not a
65
+ * problem, just a feature that is off.
66
+ */
67
+ export declare function chooseProvider(env: AiEnv): ProviderChoice;
68
+ /** The model this provider uses for this job — the operator's, or the default. */
69
+ export declare function modelFor<Task extends string>(tasks: AiTaskTable<Task>, provider: AiProviderName, task: Task, env: AiEnv): string;
70
+ export interface ResolvedAi {
71
+ provider: AiProviderName;
72
+ apiKey: string;
73
+ model: string;
74
+ }
75
+ /** Everything one call needs, or null when the feature is off. */
76
+ export declare function resolveAi<Task extends string>(tasks: AiTaskTable<Task>, task: Task, env: AiEnv): ResolvedAi | null;
77
+ export interface AiStatus<Task extends string = string> {
78
+ enabled: boolean;
79
+ provider: AiProviderName | null;
80
+ /** "Anthropic" / "OpenAI" — the name to show a person, and to name in a policy. */
81
+ providerLabel: string | null;
82
+ /** The model per job, when enabled. */
83
+ models: Record<Task, string> | null;
84
+ /** A misconfiguration worth telling an operator about, when there is one. */
85
+ problem: string | null;
86
+ }
87
+ /** What the settings and operator screens show. */
88
+ export declare function aiStatus<Task extends string>(tasks: AiTaskTable<Task>, env: AiEnv): AiStatus<Task>;
89
+ /**
90
+ * What a *person* may be told: everything but the misconfiguration.
91
+ *
92
+ * `problem` names environment variables and echoes the operator's configured
93
+ * provider — deployment detail, never a secret, but it has no business in a
94
+ * payload sent to every signed-in account (AltEd audit round 6). The operator
95
+ * screen, which is where somebody would act on it, still gets the full shape.
96
+ */
97
+ export type PublicAiStatus<Task extends string = string> = Omit<AiStatus<Task>, "problem">;
98
+ export declare function publicAiStatus<Task extends string>(tasks: AiTaskTable<Task>, env: AiEnv): PublicAiStatus<Task>;
99
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ai/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Which model provider an instance uses, and which model it uses for which job.
3
+ *
4
+ * Extracted from AltEd and PharmaLine, which had each written this file and were
5
+ * about to be joined by a third. Two rules shape it, and both are why it is
6
+ * shared rather than copied.
7
+ *
8
+ * **Naming a provider is a decision, not a preference.** If `AI_PROVIDER` names
9
+ * a provider whose key is missing, the feature is *off* and says why — it does
10
+ * not quietly send a person's records to the other one. Silently switching
11
+ * processors would break the promise a privacy policy makes by name.
12
+ *
13
+ * **Jobs, not one model.** Sorting a bank line wants a small fast model; writing
14
+ * a report wants a capable one. The app names its jobs and, for each, the
15
+ * environment variable an operator sets per provider and a working default.
16
+ * The model name is provider-specific, so pointing `AI_PROVIDER` at OpenAI can
17
+ * never send a Claude model name to OpenAI.
18
+ *
19
+ * Deliberately free of `server-only` and of anything Next-specific: PharmaLine
20
+ * reads it from a plain Node service. An app binds it from a server-only
21
+ * module of its own, because the provider half reads API keys.
22
+ */
23
+ export const AI_PROVIDERS = ["ANTHROPIC", "OPENAI"];
24
+ /** How each provider is named to a person — in a privacy policy, in settings, in logs. */
25
+ export const PROVIDER_LABELS = {
26
+ ANTHROPIC: "Anthropic",
27
+ OPENAI: "OpenAI",
28
+ };
29
+ /** The environment variable holding each provider's key. */
30
+ export const PROVIDER_KEY_VARS = {
31
+ ANTHROPIC: "ANTHROPIC_API_KEY",
32
+ OPENAI: "OPENAI_API_KEY",
33
+ };
34
+ const read = (env, name) => (env[name] ?? "").trim();
35
+ /** Providers with a key set, in preference order. */
36
+ export function configuredProviders(env) {
37
+ return AI_PROVIDERS.filter((provider) => read(env, PROVIDER_KEY_VARS[provider]).length > 0);
38
+ }
39
+ /**
40
+ * The provider this instance will use.
41
+ *
42
+ * `AI_PROVIDER` unset means "whichever is configured", preferring Anthropic when
43
+ * both are — an arbitrary but stable order, so adding a second key never silently
44
+ * moves an instance to a different processor. Nothing configured at all is not a
45
+ * problem, just a feature that is off.
46
+ */
47
+ export function chooseProvider(env) {
48
+ const requested = read(env, "AI_PROVIDER").toUpperCase();
49
+ if (requested) {
50
+ const named = AI_PROVIDERS.find((provider) => provider === requested);
51
+ if (!named) {
52
+ return {
53
+ provider: null,
54
+ apiKey: "",
55
+ problem: `AI_PROVIDER is set to “${read(env, "AI_PROVIDER")}” — expected ${AI_PROVIDERS.map((p) => p.toLowerCase()).join(" or ")}.`,
56
+ };
57
+ }
58
+ const apiKey = read(env, PROVIDER_KEY_VARS[named]);
59
+ if (!apiKey) {
60
+ return {
61
+ provider: null,
62
+ apiKey: "",
63
+ problem: `AI_PROVIDER names ${PROVIDER_LABELS[named]}, but ${PROVIDER_KEY_VARS[named]} is empty.`,
64
+ };
65
+ }
66
+ return { provider: named, apiKey, problem: null };
67
+ }
68
+ const [first] = configuredProviders(env);
69
+ if (!first)
70
+ return { provider: null, apiKey: "", problem: null };
71
+ return { provider: first, apiKey: read(env, PROVIDER_KEY_VARS[first]), problem: null };
72
+ }
73
+ /** The model this provider uses for this job — the operator's, or the default. */
74
+ export function modelFor(tasks, provider, task, env) {
75
+ const spec = tasks[task][provider];
76
+ return read(env, spec.modelVar) || spec.defaultModel;
77
+ }
78
+ /** Everything one call needs, or null when the feature is off. */
79
+ export function resolveAi(tasks, task, env) {
80
+ const choice = chooseProvider(env);
81
+ if (!choice.provider)
82
+ return null;
83
+ return { provider: choice.provider, apiKey: choice.apiKey, model: modelFor(tasks, choice.provider, task, env) };
84
+ }
85
+ /** What the settings and operator screens show. */
86
+ export function aiStatus(tasks, env) {
87
+ const choice = chooseProvider(env);
88
+ if (!choice.provider) {
89
+ return { enabled: false, provider: null, providerLabel: null, models: null, problem: choice.problem };
90
+ }
91
+ const provider = choice.provider;
92
+ const models = Object.fromEntries(Object.keys(tasks).map((task) => [task, modelFor(tasks, provider, task, env)]));
93
+ return { enabled: true, provider, providerLabel: PROVIDER_LABELS[provider], models, problem: null };
94
+ }
95
+ export function publicAiStatus(tasks, env) {
96
+ const { enabled, provider, providerLabel, models } = aiStatus(tasks, env);
97
+ return { enabled, provider, providerLabel, models };
98
+ }
99
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/ai/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { AI_PROVIDERS, PROVIDER_KEY_VARS, PROVIDER_LABELS, aiStatus, chooseProvider, configuredProviders, modelFor, publicAiStatus, resolveAi, type AiEnv, type AiModelSpec, type AiProviderName, type AiStatus, type AiTaskSpec, type AiTaskTable, type ProviderChoice, type PublicAiStatus, type ResolvedAi, } from "./config.js";
2
+ export { AI_IMAGE_TYPES, AiServiceError, AiUnavailableError, AiUnusableAnswerError, OPENAI_MIN_OUTPUT_TOKENS, createAiHarness, isAiImageType, type AiCallResult, type AiHarness, type AiHarnessOptions, type AiImage, type AiImageType, type AiStructuredCall, type AiTextCall, } from "./provider.js";
3
+ export { DEFAULT_PRICES, costUsd, findPrice, modelFamily, type ModelPrice, type PriceRates, type TokenCounts, } from "./pricing.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,cAAc,EACd,SAAS,EACT,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,wBAAwB,EACxB,eAAe,EACf,aAAa,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,cAAc,EACd,OAAO,EACP,SAAS,EACT,WAAW,EACX,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { AI_PROVIDERS, PROVIDER_KEY_VARS, PROVIDER_LABELS, aiStatus, chooseProvider, configuredProviders, modelFor, publicAiStatus, resolveAi, } from "./config.js";
2
+ export { AI_IMAGE_TYPES, AiServiceError, AiUnavailableError, AiUnusableAnswerError, OPENAI_MIN_OUTPUT_TOKENS, createAiHarness, isAiImageType, } from "./provider.js";
3
+ export { DEFAULT_PRICES, costUsd, findPrice, modelFamily, } from "./pricing.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,cAAc,EACd,SAAS,GAUV,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,wBAAwB,EACxB,eAAe,EACf,aAAa,GAQd,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,cAAc,EACd,OAAO,EACP,SAAS,EACT,WAAW,GAIZ,MAAM,cAAc,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { AiProviderName } from "./config.js";
2
+ /**
3
+ * What a model call costs, at the provider's published rates.
4
+ *
5
+ * The arithmetic and the starting rates only. Where the rates are *kept* is each
6
+ * app's decision — AltEd and PharmaLine keep a dated price table an operator can
7
+ * correct; a smaller app can price straight from `DEFAULT_PRICES` — so nothing
8
+ * here touches a database. Nothing here is the source of truth for a price
9
+ * either: the provider's pricing page is, which is why every row records where
10
+ * it came from and when.
11
+ */
12
+ export interface PriceRates {
13
+ inputPerMillionUsd: number;
14
+ outputPerMillionUsd: number;
15
+ cacheReadPerMillionUsd: number;
16
+ cacheWritePerMillionUsd: number;
17
+ }
18
+ export interface ModelPrice extends PriceRates {
19
+ provider: AiProviderName;
20
+ model: string;
21
+ source: string;
22
+ }
23
+ /**
24
+ * The rates the portfolio's apps start from — the union of AltEd's and
25
+ * PharmaLine's seeded tables. A model missing from here is unpriced, not free:
26
+ * `findPrice` returns null and the caller records the tokens without a cost.
27
+ */
28
+ export declare const DEFAULT_PRICES: readonly ModelPrice[];
29
+ /**
30
+ * The family a reported model id belongs to.
31
+ *
32
+ * Providers report dated snapshots (`claude-haiku-4-5-20251001`) and priced
33
+ * variants (`gpt-5-2026-03-01`) of a family that is priced once. The date
34
+ * suffix is dropped for the lookup, so a row for the family covers them all —
35
+ * and an exact row, when one exists, still wins.
36
+ */
37
+ export declare function modelFamily(model: string): string;
38
+ /** The rate for a model from a list: an exact row first, then its family's. Null when none is known. */
39
+ export declare function findPrice<P extends {
40
+ provider: AiProviderName;
41
+ model: string;
42
+ }>(prices: readonly P[], provider: AiProviderName, model: string): P | null;
43
+ export interface TokenCounts {
44
+ inputTokens: number;
45
+ outputTokens: number;
46
+ cacheReadTokens?: number;
47
+ cacheWriteTokens?: number;
48
+ }
49
+ /** The cost of a call at a rate, in USD. Six decimals: a Haiku call is fractions of a cent. */
50
+ export declare function costUsd(tokens: TokenCounts, rates: PriceRates): number;
51
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/ai/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;GASG;AAEH,MAAM,WAAW,UAAU;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,UAAU,EAQ9C,CAAC;AAEH;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wGAAwG;AACxG,wBAAgB,SAAS,CAAC,CAAC,SAAS;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAC7E,MAAM,EAAE,SAAS,CAAC,EAAE,EACpB,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,MAAM,GACZ,CAAC,GAAG,IAAI,CAMV;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,+FAA+F;AAC/F,wBAAgB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAQtE"}
@@ -0,0 +1,65 @@
1
+ const ANTHROPIC_SOURCE = "Anthropic API pricing, checked 2026-06-24 — cache reads 10% of input, cache writes 125%";
2
+ const OPENAI_SOURCE = "OpenAI API pricing, checked 2026-09-07 — cached input 10% of input, no write premium";
3
+ /** Per million tokens, USD. Cache multipliers are each provider's published rule. */
4
+ const anthropic = (model, input, output) => ({
5
+ provider: "ANTHROPIC",
6
+ model,
7
+ inputPerMillionUsd: input,
8
+ outputPerMillionUsd: output,
9
+ cacheReadPerMillionUsd: input * 0.1,
10
+ cacheWritePerMillionUsd: input * 1.25,
11
+ source: ANTHROPIC_SOURCE,
12
+ });
13
+ const openai = (model, input, output) => ({
14
+ provider: "OPENAI",
15
+ model,
16
+ inputPerMillionUsd: input,
17
+ outputPerMillionUsd: output,
18
+ cacheReadPerMillionUsd: input * 0.1,
19
+ cacheWritePerMillionUsd: input,
20
+ source: OPENAI_SOURCE,
21
+ });
22
+ /**
23
+ * The rates the portfolio's apps start from — the union of AltEd's and
24
+ * PharmaLine's seeded tables. A model missing from here is unpriced, not free:
25
+ * `findPrice` returns null and the caller records the tokens without a cost.
26
+ */
27
+ export const DEFAULT_PRICES = Object.freeze([
28
+ anthropic("claude-opus-5", 5, 25),
29
+ anthropic("claude-sonnet-5", 2, 10),
30
+ anthropic("claude-haiku-4-5", 1, 5),
31
+ anthropic("claude-opus-4-8", 5, 25),
32
+ anthropic("claude-sonnet-4-6", 3, 15),
33
+ openai("gpt-5", 1.25, 10),
34
+ openai("gpt-5-mini", 0.25, 2),
35
+ ]);
36
+ /**
37
+ * The family a reported model id belongs to.
38
+ *
39
+ * Providers report dated snapshots (`claude-haiku-4-5-20251001`) and priced
40
+ * variants (`gpt-5-2026-03-01`) of a family that is priced once. The date
41
+ * suffix is dropped for the lookup, so a row for the family covers them all —
42
+ * and an exact row, when one exists, still wins.
43
+ */
44
+ export function modelFamily(model) {
45
+ return model.replace(/[-_]?\d{4}-?\d{2}-?\d{2}$/, "");
46
+ }
47
+ /** The rate for a model from a list: an exact row first, then its family's. Null when none is known. */
48
+ export function findPrice(prices, provider, model) {
49
+ for (const candidate of [...new Set([model, modelFamily(model)])]) {
50
+ const row = prices.find((price) => price.provider === provider && price.model === candidate);
51
+ if (row)
52
+ return row;
53
+ }
54
+ return null;
55
+ }
56
+ /** The cost of a call at a rate, in USD. Six decimals: a Haiku call is fractions of a cent. */
57
+ export function costUsd(tokens, rates) {
58
+ const per = (count, rate) => (count / 1_000_000) * rate;
59
+ const total = per(tokens.inputTokens, rates.inputPerMillionUsd) +
60
+ per(tokens.outputTokens, rates.outputPerMillionUsd) +
61
+ per(tokens.cacheReadTokens ?? 0, rates.cacheReadPerMillionUsd) +
62
+ per(tokens.cacheWriteTokens ?? 0, rates.cacheWritePerMillionUsd);
63
+ return Math.round(total * 1_000_000) / 1_000_000;
64
+ }
65
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/ai/pricing.ts"],"names":[],"mappings":"AA0BA,MAAM,gBAAgB,GAAG,yFAAyF,CAAC;AACnH,MAAM,aAAa,GAAG,sFAAsF,CAAC;AAE7G,qFAAqF;AACrF,MAAM,SAAS,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,MAAc,EAAc,EAAE,CAAC,CAAC;IAC/E,QAAQ,EAAE,WAAW;IACrB,KAAK;IACL,kBAAkB,EAAE,KAAK;IACzB,mBAAmB,EAAE,MAAM;IAC3B,sBAAsB,EAAE,KAAK,GAAG,GAAG;IACnC,uBAAuB,EAAE,KAAK,GAAG,IAAI;IACrC,MAAM,EAAE,gBAAgB;CACzB,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,MAAc,EAAc,EAAE,CAAC,CAAC;IAC5E,QAAQ,EAAE,QAAQ;IAClB,KAAK;IACL,kBAAkB,EAAE,KAAK;IACzB,mBAAmB,EAAE,MAAM;IAC3B,sBAAsB,EAAE,KAAK,GAAG,GAAG;IACnC,uBAAuB,EAAE,KAAK;IAC9B,MAAM,EAAE,aAAa;CACtB,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAA0B,MAAM,CAAC,MAAM,CAAC;IACjE,SAAS,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,CAAC;IACjC,SAAS,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC;IACnC,SAAS,CAAC,kBAAkB,EAAE,CAAC,EAAE,CAAC,CAAC;IACnC,SAAS,CAAC,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC;IACnC,SAAS,CAAC,mBAAmB,EAAE,CAAC,EAAE,EAAE,CAAC;IACrC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;IACzB,MAAM,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;CAC9B,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,SAAS,CACvB,MAAoB,EACpB,QAAwB,EACxB,KAAa;IAEb,KAAK,MAAM,SAAS,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC;QAC7F,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;IACtB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AASD,+FAA+F;AAC/F,MAAM,UAAU,OAAO,CAAC,MAAmB,EAAE,KAAiB;IAC5D,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,IAAY,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IACxE,MAAM,KAAK,GACT,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,kBAAkB,CAAC;QACjD,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,mBAAmB,CAAC;QACnD,GAAG,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,EAAE,KAAK,CAAC,sBAAsB,CAAC;QAC9D,GAAG,CAAC,MAAM,CAAC,gBAAgB,IAAI,CAAC,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAC;IACnE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;AACnD,CAAC"}
@@ -0,0 +1,146 @@
1
+ import type { z } from "zod";
2
+ import { type AiEnv, type AiProviderName, type AiStatus, type AiTaskTable, type PublicAiStatus, type ResolvedAi } from "./config.js";
3
+ /**
4
+ * The one place either provider's SDK is called.
5
+ *
6
+ * Everything above this file asks for a job — "sort these bank lines", "write
7
+ * this month's summary" — and gets back text or a parsed object plus what it
8
+ * cost. Which company answered is a deployment decision (`config.ts`), not a
9
+ * decision any feature makes.
10
+ *
11
+ * The harness hides three real differences between the providers, and
12
+ * deliberately hides nothing else:
13
+ *
14
+ * 1. **Long requests must stream.** Both SDKs refuse a non-streaming call that
15
+ * could outlive their timeout, and transcribing a sixty-page document can.
16
+ * Structured calls stream and read the final message; nothing consumes the
17
+ * intermediate events.
18
+ * 2. **Output budgets mean different things.** An OpenAI reasoning model spends
19
+ * `max_output_tokens` on its reasoning before it writes a word, so a budget
20
+ * that is generous for Anthropic can return nothing at all. Hence the floor.
21
+ * 3. **Tuning parameters are model-specific.** Adaptive thinking and effort are
22
+ * sent only where they are asked for, and a model that rejects them produces
23
+ * a clear error naming the provider rather than a 500.
24
+ *
25
+ * Ported from AltEd's `lib/ai/provider.ts`, where it has run in production since
26
+ * tranche 113; the only change is that the jobs are the app's to name.
27
+ */
28
+ /** The feature is off, or the request never reached a model. */
29
+ export declare class AiUnavailableError extends Error {
30
+ constructor(message?: string);
31
+ }
32
+ /** The provider answered, and said no. Carries its words: an operator can act on them. */
33
+ export declare class AiServiceError extends Error {
34
+ readonly status: number | undefined;
35
+ constructor(message: string, status?: number);
36
+ }
37
+ /**
38
+ * The provider answered, but not with something usable — a refusal, a cut-off
39
+ * or an answer in the wrong shape. The tokens were spent all the same, so the
40
+ * usage travels with the error for the caller to record (AltEd round 10: these
41
+ * were free, and a failed step could be tried again without limit).
42
+ */
43
+ export declare class AiUnusableAnswerError extends AiUnavailableError {
44
+ readonly usage: AiCallResult;
45
+ constructor(message: string, usage: AiCallResult);
46
+ }
47
+ export interface AiCallResult {
48
+ provider: AiProviderName;
49
+ model: string;
50
+ inputTokens: number;
51
+ outputTokens: number;
52
+ /** Prompt tokens the provider served from its cache, and wrote to it. Billed apart. */
53
+ cacheReadTokens: number;
54
+ cacheWriteTokens: number;
55
+ }
56
+ export interface AiTextCall<Task extends string> {
57
+ task: Task;
58
+ system: string;
59
+ prompt: string;
60
+ /** Words the answer may run to. Raised automatically where reasoning shares the budget. */
61
+ maxTokens: number;
62
+ /**
63
+ * The longest the call may take, in milliseconds, with no retries — what is
64
+ * left of a serverless request, so a slow answer fails as an error rather than
65
+ * the function being killed mid-call.
66
+ */
67
+ timeoutMs?: number;
68
+ }
69
+ /** The image types both providers accept inline. */
70
+ export declare const AI_IMAGE_TYPES: readonly ["image/jpeg", "image/png", "image/gif", "image/webp"];
71
+ export type AiImageType = (typeof AI_IMAGE_TYPES)[number];
72
+ export declare function isAiImageType(value: string): value is AiImageType;
73
+ export interface AiImage {
74
+ mediaType: AiImageType;
75
+ /** Base64, no data-URL prefix. */
76
+ base64: string;
77
+ }
78
+ export interface AiStructuredCall<Task extends string, T> extends AiTextCall<Task> {
79
+ schema: z.ZodType<T>;
80
+ /** A name for the schema. OpenAI requires one; Anthropic ignores it. */
81
+ schemaName: string;
82
+ /** A PDF for the model to read. Both providers fetch it themselves. */
83
+ documentUrl?: string;
84
+ /**
85
+ * Pictures for the model to look at, sent inline and in order. Inline rather
86
+ * than a URL because most of what an app shows a model sits behind its own
87
+ * access check — there is no public URL for a provider to fetch, and there
88
+ * must not be one.
89
+ */
90
+ images?: AiImage[];
91
+ /**
92
+ * Whether to ask Anthropic for adaptive thinking. Defaults to the job's own
93
+ * `thinking` setting. OpenAI's reasoning is the model's own and is unaffected.
94
+ */
95
+ thinking?: boolean;
96
+ /**
97
+ * How hard to think, on providers and models that take the instruction.
98
+ * Anthropic-only: OpenAI's reasoning effort is left at the model's default so
99
+ * that a non-reasoning model an operator chooses still works.
100
+ */
101
+ effort?: "medium" | "high";
102
+ }
103
+ /**
104
+ * An OpenAI reasoning model bills its thinking against the same budget as its
105
+ * answer, so a 900-token ceiling can be spent entirely before the prose starts.
106
+ * Small requests get room; large ones already have it.
107
+ */
108
+ export declare const OPENAI_MIN_OUTPUT_TOKENS = 4000;
109
+ export interface AiHarnessOptions<Task extends string> {
110
+ /** The app's jobs, each with a model per provider. */
111
+ tasks: AiTaskTable<Task>;
112
+ /** Where configuration is read from, on every call. Defaults to `process.env`. */
113
+ env?: () => AiEnv;
114
+ }
115
+ export interface AiHarness<Task extends string> {
116
+ /** Everything one call would use, or null when the feature is off. Never logged: it holds the key. */
117
+ resolve(task: Task): ResolvedAi | null;
118
+ /** For an operator: includes a misconfiguration, when there is one. */
119
+ status(): AiStatus<Task>;
120
+ /** For a person: everything but the misconfiguration. */
121
+ publicStatus(): PublicAiStatus<Task>;
122
+ /** Prose, from a system prompt and one user message. */
123
+ generateText(call: AiTextCall<Task>): Promise<AiCallResult & {
124
+ text: string;
125
+ }>;
126
+ /** A parsed object in the shape of a Zod schema, optionally read from a PDF or pictures. */
127
+ generateStructured<T>(call: AiStructuredCall<Task, T>): Promise<AiCallResult & {
128
+ parsed: T;
129
+ }>;
130
+ }
131
+ /**
132
+ * Bind the harness to an app's jobs.
133
+ *
134
+ * ```ts
135
+ * export const ai = createAiHarness({
136
+ * tasks: {
137
+ * categorise: {
138
+ * ANTHROPIC: { modelVar: "ANTHROPIC_MODEL", defaultModel: "claude-haiku-4-5-20251001" },
139
+ * OPENAI: { modelVar: "OPENAI_MODEL", defaultModel: "gpt-5-mini" },
140
+ * },
141
+ * },
142
+ * });
143
+ * ```
144
+ */
145
+ export declare function createAiHarness<Task extends string>(options: AiHarnessOptions<Task>): AiHarness<Task>;
146
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,234 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
3
+ import OpenAI from "openai";
4
+ import { zodTextFormat } from "openai/helpers/zod";
5
+ import { aiStatus, PROVIDER_LABELS, publicAiStatus, resolveAi, } from "./config.js";
6
+ /**
7
+ * The one place either provider's SDK is called.
8
+ *
9
+ * Everything above this file asks for a job — "sort these bank lines", "write
10
+ * this month's summary" — and gets back text or a parsed object plus what it
11
+ * cost. Which company answered is a deployment decision (`config.ts`), not a
12
+ * decision any feature makes.
13
+ *
14
+ * The harness hides three real differences between the providers, and
15
+ * deliberately hides nothing else:
16
+ *
17
+ * 1. **Long requests must stream.** Both SDKs refuse a non-streaming call that
18
+ * could outlive their timeout, and transcribing a sixty-page document can.
19
+ * Structured calls stream and read the final message; nothing consumes the
20
+ * intermediate events.
21
+ * 2. **Output budgets mean different things.** An OpenAI reasoning model spends
22
+ * `max_output_tokens` on its reasoning before it writes a word, so a budget
23
+ * that is generous for Anthropic can return nothing at all. Hence the floor.
24
+ * 3. **Tuning parameters are model-specific.** Adaptive thinking and effort are
25
+ * sent only where they are asked for, and a model that rejects them produces
26
+ * a clear error naming the provider rather than a 500.
27
+ *
28
+ * Ported from AltEd's `lib/ai/provider.ts`, where it has run in production since
29
+ * tranche 113; the only change is that the jobs are the app's to name.
30
+ */
31
+ /** The feature is off, or the request never reached a model. */
32
+ export class AiUnavailableError extends Error {
33
+ constructor(message = "AI isn't available on this instance — no model is configured.") {
34
+ super(message);
35
+ this.name = "AiUnavailableError";
36
+ }
37
+ }
38
+ /** The provider answered, and said no. Carries its words: an operator can act on them. */
39
+ export class AiServiceError extends Error {
40
+ status;
41
+ constructor(message, status) {
42
+ super(message);
43
+ this.name = "AiServiceError";
44
+ this.status = status;
45
+ }
46
+ }
47
+ /**
48
+ * The provider answered, but not with something usable — a refusal, a cut-off
49
+ * or an answer in the wrong shape. The tokens were spent all the same, so the
50
+ * usage travels with the error for the caller to record (AltEd round 10: these
51
+ * were free, and a failed step could be tried again without limit).
52
+ */
53
+ export class AiUnusableAnswerError extends AiUnavailableError {
54
+ usage;
55
+ constructor(message, usage) {
56
+ super(message);
57
+ this.name = "AiUnusableAnswerError";
58
+ this.usage = usage;
59
+ }
60
+ }
61
+ /** The image types both providers accept inline. */
62
+ export const AI_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
63
+ export function isAiImageType(value) {
64
+ return AI_IMAGE_TYPES.includes(value);
65
+ }
66
+ /**
67
+ * An OpenAI reasoning model bills its thinking against the same budget as its
68
+ * answer, so a 900-token ceiling can be spent entirely before the prose starts.
69
+ * Small requests get room; large ones already have it.
70
+ */
71
+ export const OPENAI_MIN_OUTPUT_TOKENS = 4000;
72
+ /** Turn either SDK's error into one a route can report in the provider's own words. */
73
+ function asServiceError(error, provider) {
74
+ if (error instanceof Anthropic.APIError || error instanceof OpenAI.APIError) {
75
+ // The two SDKs type `status` differently; narrow rather than trust either.
76
+ const status = typeof error.status === "number" ? error.status : undefined;
77
+ throw new AiServiceError(`${PROVIDER_LABELS[provider]} refused the request (${status ?? "no status"}): ${error.message}`, status);
78
+ }
79
+ throw error;
80
+ }
81
+ /** OpenAI counts cached prompt tokens inside `input_tokens`; move them out rather than bill them twice. */
82
+ function openAiUsage(provider, model, usage) {
83
+ const cached = usage?.input_tokens_details?.cached_tokens ?? 0;
84
+ return {
85
+ provider,
86
+ model,
87
+ inputTokens: (usage?.input_tokens ?? 0) - cached,
88
+ outputTokens: usage?.output_tokens ?? 0,
89
+ cacheReadTokens: cached,
90
+ cacheWriteTokens: 0,
91
+ };
92
+ }
93
+ function anthropicUsage(provider, message) {
94
+ return {
95
+ provider,
96
+ model: message.model,
97
+ inputTokens: message.usage.input_tokens,
98
+ outputTokens: message.usage.output_tokens,
99
+ cacheReadTokens: message.usage.cache_read_input_tokens ?? 0,
100
+ cacheWriteTokens: message.usage.cache_creation_input_tokens ?? 0,
101
+ };
102
+ }
103
+ /**
104
+ * Bind the harness to an app's jobs.
105
+ *
106
+ * ```ts
107
+ * export const ai = createAiHarness({
108
+ * tasks: {
109
+ * categorise: {
110
+ * ANTHROPIC: { modelVar: "ANTHROPIC_MODEL", defaultModel: "claude-haiku-4-5-20251001" },
111
+ * OPENAI: { modelVar: "OPENAI_MODEL", defaultModel: "gpt-5-mini" },
112
+ * },
113
+ * },
114
+ * });
115
+ * ```
116
+ */
117
+ export function createAiHarness(options) {
118
+ const { tasks } = options;
119
+ const env = options.env ?? (() => process.env);
120
+ function resolve(task) {
121
+ const config = resolveAi(tasks, task, env());
122
+ if (!config)
123
+ throw new AiUnavailableError();
124
+ return config;
125
+ }
126
+ async function generateText(call) {
127
+ const { provider, apiKey, model } = resolve(call.task);
128
+ const requestOptions = call.timeoutMs ? { timeout: call.timeoutMs, maxRetries: 0 } : undefined;
129
+ try {
130
+ if (provider === "ANTHROPIC") {
131
+ const message = await new Anthropic({ apiKey }).messages.create({
132
+ model,
133
+ max_tokens: call.maxTokens,
134
+ system: call.system,
135
+ messages: [{ role: "user", content: call.prompt }],
136
+ }, requestOptions);
137
+ const text = message.content
138
+ .filter((block) => block.type === "text")
139
+ .map((block) => block.text)
140
+ .join("\n")
141
+ .trim();
142
+ return { text, ...anthropicUsage(provider, message) };
143
+ }
144
+ const response = await new OpenAI({ apiKey }).responses.create({
145
+ model,
146
+ instructions: call.system,
147
+ input: call.prompt,
148
+ max_output_tokens: Math.max(call.maxTokens, OPENAI_MIN_OUTPUT_TOKENS),
149
+ }, requestOptions);
150
+ return { text: response.output_text.trim(), ...openAiUsage(provider, response.model, response.usage) };
151
+ }
152
+ catch (error) {
153
+ asServiceError(error, provider);
154
+ }
155
+ }
156
+ async function generateStructured(call) {
157
+ const { provider, apiKey, model } = resolve(call.task);
158
+ const requestOptions = call.timeoutMs ? { timeout: call.timeoutMs, maxRetries: 0 } : undefined;
159
+ const thinking = call.thinking ?? tasks[call.task].thinking ?? false;
160
+ try {
161
+ if (provider === "ANTHROPIC") {
162
+ const content = [];
163
+ if (call.documentUrl)
164
+ content.push({ type: "document", source: { type: "url", url: call.documentUrl } });
165
+ for (const image of call.images ?? []) {
166
+ content.push({
167
+ type: "image",
168
+ source: { type: "base64", media_type: image.mediaType, data: image.base64 },
169
+ });
170
+ }
171
+ content.push({ type: "text", text: call.prompt });
172
+ // Streamed, not because anything reads the stream, but because the SDK
173
+ // refuses a non-streaming call that could run past ten minutes.
174
+ const stream = new Anthropic({ apiKey }).messages.stream({
175
+ model,
176
+ max_tokens: call.maxTokens,
177
+ system: call.system,
178
+ ...(thinking ? { thinking: { type: "adaptive" } } : {}),
179
+ output_config: { ...(call.effort ? { effort: call.effort } : {}), format: zodOutputFormat(call.schema) },
180
+ messages: [{ role: "user", content }],
181
+ }, requestOptions);
182
+ const response = await stream.finalMessage();
183
+ const usage = anthropicUsage(provider, response);
184
+ if (response.stop_reason === "refusal") {
185
+ throw new AiUnusableAnswerError("The model declined to process this request.", usage);
186
+ }
187
+ if (response.parsed_output === null || response.parsed_output === undefined) {
188
+ throw new AiUnusableAnswerError("The model's answer did not match the expected shape.", usage);
189
+ }
190
+ return { parsed: response.parsed_output, ...usage };
191
+ }
192
+ const content = [];
193
+ if (call.documentUrl)
194
+ content.push({ type: "input_file", file_url: call.documentUrl });
195
+ for (const image of call.images ?? []) {
196
+ content.push({
197
+ type: "input_image",
198
+ image_url: `data:${image.mediaType};base64,${image.base64}`,
199
+ detail: "auto",
200
+ });
201
+ }
202
+ content.push({ type: "input_text", text: call.prompt });
203
+ const stream = new OpenAI({ apiKey }).responses.stream({
204
+ model,
205
+ instructions: call.system,
206
+ input: [{ role: "user", content }],
207
+ max_output_tokens: Math.max(call.maxTokens, OPENAI_MIN_OUTPUT_TOKENS),
208
+ text: { format: zodTextFormat(call.schema, call.schemaName) },
209
+ }, requestOptions);
210
+ const response = await stream.finalResponse();
211
+ const usage = openAiUsage(provider, response.model, response.usage);
212
+ if (response.status === "incomplete") {
213
+ throw new AiUnusableAnswerError(`The answer was cut off before it was complete (${response.incomplete_details?.reason ?? "no reason given"}). Try a model with more room, or a shorter input.`, usage);
214
+ }
215
+ if (response.output_parsed === null || response.output_parsed === undefined) {
216
+ throw new AiUnusableAnswerError("The model's answer did not match the expected shape.", usage);
217
+ }
218
+ return { parsed: response.output_parsed, ...usage };
219
+ }
220
+ catch (error) {
221
+ if (error instanceof AiUnavailableError)
222
+ throw error;
223
+ asServiceError(error, provider);
224
+ }
225
+ }
226
+ return {
227
+ resolve: (task) => resolveAi(tasks, task, env()),
228
+ status: () => aiStatus(tasks, env()),
229
+ publicStatus: () => publicAiStatus(tasks, env()),
230
+ generateText,
231
+ generateStructured,
232
+ };
233
+ }
234
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@connextar/house",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "The pieces every app we build needs and none of them should own a copy of.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,10 @@
14
14
  "docs"
15
15
  ],
16
16
  "exports": {
17
+ "./ai": {
18
+ "types": "./dist/ai/index.d.ts",
19
+ "default": "./dist/ai/index.js"
20
+ },
17
21
  "./errors": {
18
22
  "types": "./dist/errors/index.d.ts",
19
23
  "default": "./dist/errors/index.js"
@@ -103,10 +107,13 @@
103
107
  "check:ci": "npm run lint:check && npm run format:check && npm run typecheck"
104
108
  },
105
109
  "peerDependencies": {
110
+ "@anthropic-ai/sdk": ">=0.122.0",
106
111
  "next": "^16.0.0",
112
+ "openai": ">=7.10.0",
107
113
  "react": "^19.0.0",
108
114
  "react-dom": "^19.0.0",
109
- "react-hook-form": "^7.60.0"
115
+ "react-hook-form": "^7.60.0",
116
+ "zod": "^3.25.0 || ^4.0.0"
110
117
  },
111
118
  "peerDependenciesMeta": {
112
119
  "next": {
@@ -114,9 +121,19 @@
114
121
  },
115
122
  "react-hook-form": {
116
123
  "optional": true
124
+ },
125
+ "@anthropic-ai/sdk": {
126
+ "optional": true
127
+ },
128
+ "openai": {
129
+ "optional": true
130
+ },
131
+ "zod": {
132
+ "optional": true
117
133
  }
118
134
  },
119
135
  "devDependencies": {
136
+ "@anthropic-ai/sdk": "^0.128.0",
120
137
  "@eslint/js": "^9.39.5",
121
138
  "@testing-library/react": "^16.3.3",
122
139
  "@types/node": "^26.4.1",
@@ -127,13 +144,15 @@
127
144
  "eslint": "^9.0.0",
128
145
  "jsdom": "^27.4.0",
129
146
  "next": "^16.3.3",
147
+ "openai": "^7.23.0",
130
148
  "prettier": "^3.0.0",
131
149
  "react": "^19.2.8",
132
150
  "react-dom": "^19.2.8",
133
151
  "react-hook-form": "^7.68.0",
134
152
  "typescript": "^5.7.0",
135
153
  "typescript-eslint": "^8.69.0",
136
- "vitest": "^4.0.0"
154
+ "vitest": "^4.0.0",
155
+ "zod": "^4.6.5"
137
156
  },
138
157
  "publishConfig": {
139
158
  "access": "public"