@plumbus/ai-decision-openai 0.0.0-stage → 0.2.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,20 @@
1
+ import { type DecisionHttpConfig, type DecisionProviderAdapter } from '@plumbus/ai-decision';
2
+ export interface OpenAIDecisionAdapterConfig extends Omit<DecisionHttpConfig, 'baseUrl' | 'apiKey'> {
3
+ apiKey: string;
4
+ /** API prefix, normally https://api.openai.com/v1. */
5
+ baseUrl?: string;
6
+ model?: string;
7
+ /** Input USD / million tokens, keyed by the actual response model ID. */
8
+ inputRates?: Readonly<Record<string, number>>;
9
+ }
10
+ /**
11
+ * Source: https://developers.openai.com/api/docs/guides/decisions, verified 2026-10-08.
12
+ * Output and cached input are not charged separately. Bundled rates double above
13
+ * 272K input tokens; regional processing is not modeled.
14
+ */
15
+ export declare const OpenAIDecisionInputRates: Readonly<{
16
+ 'gpt-6-luna': 0.1;
17
+ }>;
18
+ /** Creates an OpenAI Decisions API (`POST /v1/decisions`) adapter without the OpenAI SDK. */
19
+ export declare function createOpenAIDecisionAdapter(config: OpenAIDecisionAdapterConfig): DecisionProviderAdapter;
20
+ //# sourceMappingURL=openai-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-adapter.d.ts","sourceRoot":"","sources":["../src/openai-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,kBAAkB,EACvB,KAAK,uBAAuB,EAG7B,MAAM,sBAAsB,CAAC;AAG9B,MAAM,WAAW,2BACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;EAAuC,CAAC;AA8E7E,6FAA6F;AAC7F,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,2BAA2B,GAClC,uBAAuB,CA+LzB"}
@@ -0,0 +1,246 @@
1
+ import { createDecisionHttpTransport, DecisionJsonSchema, DecisionProviderError, parseDecisionResponse, validateDecisionRequest, } from '@plumbus/ai-decision';
2
+ import { z } from '@plumbus/core/zod';
3
+ /**
4
+ * Source: https://developers.openai.com/api/docs/guides/decisions, verified 2026-10-08.
5
+ * Output and cached input are not charged separately. Bundled rates double above
6
+ * 272K input tokens; regional processing is not modeled.
7
+ */
8
+ export const OpenAIDecisionInputRates = Object.freeze({ 'gpt-6-luna': 0.1 });
9
+ // https://developers.openai.com/api/docs/models/gpt-6-luna: "more than 272K input tokens are priced at 2x".
10
+ const LongContextInputTokens = 272_000;
11
+ // OpenAI documents no precision; its examples use two decimal places.
12
+ const Rounding = 0.01;
13
+ const Provider = 'openai-decisions';
14
+ const ProbabilitySchema = z.number().finite().min(0).max(1);
15
+ const TokensSchema = z.number().int().nonnegative().safe();
16
+ const MetadataSchema = z.object({
17
+ model: z.string().trim().min(1).max(512),
18
+ usage: z.object({ input_tokens: TokensSchema, output_tokens: TokensSchema }),
19
+ });
20
+ const EnvelopeSchema = MetadataSchema.extend({ answers: z.array(z.unknown()).max(256) });
21
+ const AnswerSchema = z.discriminatedUnion('type', [
22
+ z.object({ type: z.literal('predicate'), name: z.string(), probability: ProbabilitySchema }),
23
+ z.object({
24
+ type: z.literal('choice'),
25
+ name: z.string(),
26
+ choice: z.string(),
27
+ confidence: ProbabilitySchema,
28
+ probabilities: z
29
+ .array(z.object({ value: z.string(), probability: ProbabilitySchema }))
30
+ .max(255),
31
+ }),
32
+ z.object({
33
+ type: z.literal('score'),
34
+ name: z.string(),
35
+ score: z.number().finite(),
36
+ confidence: ProbabilitySchema,
37
+ probabilities: z
38
+ .array(z.object({
39
+ value: z.number().int().nonnegative(),
40
+ label: z.string(),
41
+ probability: ProbabilitySchema,
42
+ }))
43
+ .max(10),
44
+ }),
45
+ z.object({ type: z.literal('refusal'), name: z.string().nullable() }),
46
+ ]);
47
+ const text = (value) => typeof value === 'string' ? value : JSON.stringify(value);
48
+ function toOpenAIQuestions(questions) {
49
+ return Object.entries(questions).map(([name, question]) => {
50
+ const instructions = text(question.instructions);
51
+ if (question.type === 'choice')
52
+ return {
53
+ type: 'choice',
54
+ name,
55
+ instructions,
56
+ choices: Object.entries(question.criteria).map(([value, description]) => description === null ? { value } : { value, description: text(description) }),
57
+ };
58
+ if (question.type === 'score')
59
+ return {
60
+ type: 'score',
61
+ name,
62
+ instructions,
63
+ levels: question.criteria.map((level) => ({ label: text(level) })),
64
+ };
65
+ const criteria = question.criteria ?? {};
66
+ return {
67
+ type: 'predicate',
68
+ name,
69
+ instructions: [
70
+ instructions,
71
+ ...(criteria.true === undefined ? [] : [`True when: ${text(criteria.true)}`]),
72
+ ...(criteria.false === undefined ? [] : [`False when: ${text(criteria.false)}`]),
73
+ ].join('\n'),
74
+ };
75
+ });
76
+ }
77
+ /** Creates an OpenAI Decisions API (`POST /v1/decisions`) adapter without the OpenAI SDK. */
78
+ export function createOpenAIDecisionAdapter(config) {
79
+ const settings = z
80
+ .object({
81
+ apiKey: z.string().min(1),
82
+ model: z.string().trim().min(1).max(256).default('gpt-6-luna'),
83
+ inputRates: DecisionJsonSchema.pipe(z.record(z.number().finite().nonnegative())).optional(),
84
+ })
85
+ .safeParse(config);
86
+ if (!settings.success)
87
+ throw new DecisionProviderError(Provider, 'configuration', 'OpenAI requires an API key and valid model/pricing configuration');
88
+ const rates = {
89
+ ...OpenAIDecisionInputRates,
90
+ ...settings.data.inputRates,
91
+ };
92
+ const post = createDecisionHttpTransport(Provider, {
93
+ ...config,
94
+ apiKey: settings.data.apiKey,
95
+ baseUrl: config.baseUrl === undefined ? 'https://api.openai.com/v1' : config.baseUrl,
96
+ }, { path: 'decisions' });
97
+ // Dated snapshots (gpt-6-luna-2026-10-01) inherit the alias rate; other unknown models stay unpriced.
98
+ const pricedAs = (model) => {
99
+ if (Object.hasOwn(rates, model))
100
+ return model;
101
+ const alias = /^(.+)-\d{4}-\d{2}-\d{2}$/.exec(model)?.[1];
102
+ return alias !== undefined && Object.hasOwn(rates, alias) ? alias : undefined;
103
+ };
104
+ const costFor = (model, usage) => {
105
+ const priced = pricedAs(model);
106
+ const rate = priced === undefined ? undefined : rates[priced];
107
+ if (priced === undefined || rate === undefined)
108
+ return null;
109
+ // Configured rates are flat; bundled rates double for long prompts, as OpenAI bills them.
110
+ const long = !Object.hasOwn(settings.data.inputRates ?? {}, priced) &&
111
+ usage.inputTokens > LongContextInputTokens;
112
+ const cost = (usage.inputTokens / 1_000_000) * rate * (long ? 2 : 1);
113
+ return Number.isFinite(cost) && (cost > 0 || usage.inputTokens === 0 || rate === 0)
114
+ ? cost
115
+ : Number.NaN;
116
+ };
117
+ return {
118
+ name: Provider,
119
+ rounding: Rounding,
120
+ async decide(request) {
121
+ const input = validateDecisionRequest(request, Provider);
122
+ const start = performance.now();
123
+ const wire = await post({
124
+ model: input.model ?? settings.data.model,
125
+ input: text(input.state),
126
+ questions: toOpenAIQuestions(input.questions),
127
+ }, input);
128
+ const metadata = MetadataSchema.safeParse(wire);
129
+ const model = metadata.success ? metadata.data.model : undefined;
130
+ const usage = metadata.success
131
+ ? {
132
+ inputTokens: metadata.data.usage.input_tokens,
133
+ outputTokens: metadata.data.usage.output_tokens,
134
+ totalTokens: metadata.data.usage.input_tokens + metadata.data.usage.output_tokens,
135
+ }
136
+ : undefined;
137
+ const fail = (message, refusedQuestions) => {
138
+ const refusal = refusedQuestions === undefined ? {} : { refusedQuestions };
139
+ if (model === undefined || usage === undefined || !Number.isSafeInteger(usage.totalTokens))
140
+ return new DecisionProviderError(Provider, 'invalid_response', message, refusal);
141
+ const cost = costFor(model, usage);
142
+ return new DecisionProviderError(Provider, 'invalid_response', message, {
143
+ model,
144
+ usage,
145
+ cost: cost === null || Number.isNaN(cost) ? null : cost,
146
+ ...refusal,
147
+ });
148
+ };
149
+ const envelope = EnvelopeSchema.safeParse(wire);
150
+ if (!envelope.success)
151
+ throw fail('Invalid OpenAI decision response envelope');
152
+ const mismatch = 'OpenAI decision answers do not match the question contract';
153
+ if (envelope.data.answers.length !== Object.keys(input.questions).length)
154
+ throw fail(mismatch);
155
+ const answers = {};
156
+ const named = new Set();
157
+ let refused = false;
158
+ for (const raw of envelope.data.answers) {
159
+ const parsed = AnswerSchema.safeParse(raw);
160
+ if (!parsed.success)
161
+ throw fail(mismatch);
162
+ const answer = parsed.data;
163
+ if (answer.type === 'refusal')
164
+ refused = true;
165
+ // Only a refusal may be unnamed; its question is found by elimination below.
166
+ if (answer.name === null)
167
+ continue;
168
+ const question = Object.hasOwn(input.questions, answer.name)
169
+ ? input.questions[answer.name]
170
+ : undefined;
171
+ if (question === undefined || named.has(answer.name))
172
+ throw fail(mismatch);
173
+ named.add(answer.name);
174
+ if (answer.type === 'refusal')
175
+ continue;
176
+ if (answer.type === 'predicate') {
177
+ answers[answer.name] = { type: 'noul', noul: answer.probability };
178
+ }
179
+ else if (answer.type === 'choice') {
180
+ if (question.type !== 'choice')
181
+ throw fail(mismatch);
182
+ // Omitted options have zero probability; unknown or repeated options are errors.
183
+ const probabilities = Object.fromEntries(Object.keys(question.criteria).map((value) => [value, 0]));
184
+ const seen = new Set();
185
+ for (const option of answer.probabilities) {
186
+ if (!Object.hasOwn(probabilities, option.value) || seen.has(option.value))
187
+ throw fail(mismatch);
188
+ seen.add(option.value);
189
+ probabilities[option.value] = option.probability;
190
+ }
191
+ answers[answer.name] = {
192
+ type: 'choice',
193
+ choice: answer.choice,
194
+ probabilities,
195
+ confidence: answer.confidence,
196
+ };
197
+ }
198
+ else {
199
+ if (question.type !== 'score')
200
+ throw fail(mismatch);
201
+ const levels = question.criteria;
202
+ const probabilities = Object.fromEntries(levels.map((_, index) => [String(index), 0]));
203
+ const seen = new Set();
204
+ for (const level of answer.probabilities) {
205
+ const requested = levels[level.value];
206
+ if (requested === undefined || level.label !== text(requested) || seen.has(level.value))
207
+ throw fail(mismatch);
208
+ seen.add(level.value);
209
+ probabilities[String(level.value)] = level.probability;
210
+ }
211
+ answers[answer.name] = {
212
+ type: 'score',
213
+ score: answer.score,
214
+ probabilities,
215
+ confidence: answer.confidence,
216
+ legend: Object.fromEntries(levels.map((level, index) => [String(index), level])),
217
+ };
218
+ }
219
+ }
220
+ // Check the answered questions first, so a malformed answer is never reported as a refusal.
221
+ const answered = Object.fromEntries(Object.entries(input.questions).filter(([key]) => Object.hasOwn(answers, key)));
222
+ let result;
223
+ try {
224
+ result = parseDecisionResponse({ model: envelope.data.model, usage: envelope.data.usage, answers }, answered, Provider, { rounding: Rounding });
225
+ }
226
+ catch (error) {
227
+ if (error instanceof DecisionProviderError)
228
+ throw fail(mismatch);
229
+ throw error;
230
+ }
231
+ // Every other question has exactly one answer, so the unanswered ones were refused.
232
+ if (refused)
233
+ throw fail('OpenAI refused to answer a decision question', Object.keys(input.questions).filter((key) => !Object.hasOwn(answers, key)));
234
+ const cost = costFor(result.model, result.usage);
235
+ if (cost !== null && Number.isNaN(cost))
236
+ throw new DecisionProviderError(Provider, 'configuration', 'OpenAI cost estimate cannot be represented', { usage: result.usage, model: result.model });
237
+ return {
238
+ ...result,
239
+ cost,
240
+ costAvailable: cost !== null,
241
+ latencyMs: performance.now() - start,
242
+ };
243
+ },
244
+ };
245
+ }
246
+ //# sourceMappingURL=openai-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-adapter.js","sourceRoot":"","sources":["../src/openai-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ # @plumbus/ai-decision-openai — agent instructions
2
+
3
+ | File | When to read |
4
+ | --- | --- |
5
+ | [framework.md](framework.md) | Installation, public exports, question mapping, adapter boundary, usage rules |
6
+ | [testing.md](testing.md) | Offline validation and live environment setup |
7
+
8
+ **Classification, categorization, provider/model selection, OpenAI Decisions:**
9
+ read `node_modules/@plumbus/core/instructions/ai-classification.md` first (core
10
+ **0.7.4+**), then [framework.md](framework.md) for this package.
11
+
12
+ OpenAI's Decisions API is in public beta; this adapter follows OpenAI's documentation as
13
+ of 2026-10-08.
14
+
15
+ Core 0.7.3+ provides `ctx.ai.decide()`; core 0.7.4+ also routes `ctx.ai.classify()`
16
+ to explicitly registered decision adapters. Keep app business logic in Plumbus
17
+ primitives and `ctx.*`. Never route predictions around capability access checks
18
+ or confirmation policy. Provider registration is explicit in `app/server.ts`,
19
+ under `decisions.providers` with a key that differs from any text provider (for
20
+ example `openai-decisions`).
21
+
22
+ [Package README](../README.md) · [Monorepo guide](https://github.com/plumbus-framework/plumbus/blob/main/docs/ai/decision-providers.md)
@@ -0,0 +1,99 @@
1
+ # Framework boundary
2
+
3
+ OpenAI Decisions API provider for typed choices, scores, and probabilities. OpenAI's
4
+ Decisions API is in public beta; this adapter follows OpenAI's documentation as of
5
+ 2026-10-08 and may need a patch release if the API changes at general availability.
6
+
7
+ ## Install and ownership
8
+
9
+ Install `@plumbus/ai-decision-openai` explicitly. Its core peer is exactly `"0.7.x"`.
10
+ Consumers reuse Zod/testing utilities from core; do not install them separately, and
11
+ do not install the `openai` SDK for this adapter. Use this package for bounded typed
12
+ decisions. Use core generation and embeddings (the `openai` text provider) for text
13
+ responses and vector inference.
14
+
15
+ Core checks every `ctx.ai` result again with its own copy of `@plumbus/ai-decision`,
16
+ and only 0.2.3+ allows for OpenAI's rounding. Core 0.7.9 depends on `~0.2.3`. On core
17
+ 0.7.3–0.7.8, run `pnpm dedupe` after installing and check that
18
+ `pnpm why @plumbus/ai-decision` lists a single 0.2.3+ copy; otherwise core rejects
19
+ OpenAI's rounded answers as `invalid_response` after the call is billed.
20
+
21
+ Public exports: `createOpenAIDecisionAdapter` — construct an explicitly configured adapter;
22
+ `OpenAIDecisionInputRates` — bundled input USD per million tokens; `DecisionProviderError` —
23
+ structured failures; `DecisionProviderAdapter`, `DecisionQuestions`, `DecisionRequest`,
24
+ `DecisionResult` — protocol types.
25
+
26
+ File map: `src/index.ts` is the public barrel, `src/__tests__/` exercises the public
27
+ API, `instructions/` contains these recipes.
28
+
29
+ ## Register the provider
30
+
31
+ ```ts
32
+ // app/server.ts
33
+ import { createOpenAIDecisionAdapter } from '@plumbus/ai-decision-openai';
34
+
35
+ export const decisions = {
36
+ providers: {
37
+ 'openai-decisions': createOpenAIDecisionAdapter({
38
+ apiKey: process.env.OPENAI_API_KEY ?? '',
39
+ model: 'gpt-6-luna',
40
+ }),
41
+ },
42
+ defaultProvider: 'openai-decisions',
43
+ };
44
+ ```
45
+
46
+ Use a registry key other than `openai`: decision and text provider names must be
47
+ distinct, and apps commonly register the `openai` text provider too. Cost rows record
48
+ the adapter's provider name, `openai-decisions`, whatever key you register it under.
49
+
50
+ Config: `apiKey` (required), `model` (default `gpt-6-luna`), `baseUrl` (default
51
+ `https://api.openai.com/v1`; the adapter appends `/decisions`), `inputRates`,
52
+ `timeoutMs`, `maxRetries`, `fetch`.
53
+
54
+ ## Question mapping
55
+
56
+ | Plumbus | OpenAI | Notes |
57
+ | --- | --- | --- |
58
+ | `probability` | `predicate` | `criteria.true` / `criteria.false` are appended as `True when:` / `False when:` lines |
59
+ | `choice` | `choice` | Criteria keys are choice values; `null` descriptions are omitted |
60
+ | `score` | `score` | Criteria are ordered level labels (lowest first); score is 0-based |
61
+
62
+ `state` becomes `input`: strings are sent unchanged; JSON objects/arrays are sent as
63
+ JSON text. Image inputs and `safety_identifier` are not supported by this adapter yet.
64
+ `probability` answers have no `confidence`; choice/score answers keep OpenAI's
65
+ `confidence` separately from the probability distribution.
66
+
67
+ OpenAI does not document the precision of its numbers. The adapter accepts answers that
68
+ are consistent within two-decimal rounding: a distribution that sums to 1 within rounding
69
+ (at most ten options' worth), a chosen option within rounding of the top option, and a
70
+ score within rounding of its distribution's weighted mean. Options or levels that OpenAI
71
+ omits get probability 0. The returned values are OpenAI's own; the chosen option is
72
+ never changed.
73
+
74
+ ## Classification and model selection (core 0.7.4+)
75
+
76
+ Read `node_modules/@plumbus/core/instructions/ai-classification.md` for the complete
77
+ registration and usage recipe. Call `ctx.ai.classify({ text, labels, provider:
78
+ 'openai-decisions', model, threshold })` inside capabilities/flows. Each label becomes
79
+ one `predicate` question in a single `/v1/decisions` request, so a refusal on any label
80
+ fails the whole `classify()` call. `threshold` defaults to 0.5 and returns all matching
81
+ labels. Use `decide()` for richer questions.
82
+
83
+ ## Required rules
84
+
85
+ 1. Keep capabilities, flows, access policy and `ctx.*` as the application implementation path.
86
+ 2. Construct adapters at a server-owned infrastructure boundary and inject them. Keep `OPENAI_API_KEY` on the server.
87
+ 3. Treat direct adapter usage as protocol integration, not a replacement for core's security/budget/audit lifecycle.
88
+ 4. With core 0.7.3+, use `ctx.ai.decide()` in capabilities/flows. Export `decisions = { providers, defaultProvider }` from `app/server.ts`; API and workers share it. Do not register decision adapters as completion providers.
89
+ 5. Preserve distributions and provider confidence separately. A high confidence value is not authorization or proof of correctness.
90
+ 6. A `refusal` answer for any question fails the whole call (`decide()` or `classify()`) with a `DecisionProviderError` of kind `invalid_response`; the call is still billed and recorded. Detect it with `error.refusedQuestions`, which is set only for refusals and lists the refused question keys (for `classify()`, `label_<index>` by position in `labels`). Handle it as a business outcome (for example route to review), not by retrying in a loop.
91
+ 7. Validate thresholds on the actual task/model/language. Unknown cost is `null`, not free. The bundled `gpt-6-luna` rate doubles above 272K input tokens, as OpenAI bills long prompts. `inputRates` entries replace the bundled rate and are applied flat; use one for regional processing (for example `{ 'gpt-6-luna': 0.11 }` for OpenAI's 10% uplift) or a different account rate.
92
+ 8. Pass `signal: ctx.signal` and `timeoutMs` where needed. Flow steps supply their signal by default.
93
+ 9. Define contracts using `defineDecision` from `@plumbus/ai-decision`; CLI startup discovers `app/decisions/`. No automatic environment provider discovery or dedicated decision CLI commands are provided.
94
+ 10. Core records one `decide` or `classify` row per dispatched logical call, including failed/cancelled calls, with tenant/actor and `costContext`. Persist it in the existing `onAICostRecorded` hook. Unknown cost is null; never convert it to free usage.
95
+
96
+ Chat, voice and MCP still consume Plumbus capabilities; this package does not
97
+ introduce a parallel runtime or transport for those surfaces.
98
+
99
+ See `docs/ai/decision-providers.md` in the monorepo for the full API and configuration.
@@ -0,0 +1,31 @@
1
+ # Testing
2
+
3
+ Use injected `fetch` for offline adapter tests. Respond with the Decisions API shape:
4
+ `{ model, usage: { input_tokens, output_tokens }, answers: [{ type, name, ... }] }`,
5
+ one answer per question, named by the Plumbus question key. In consumer applications,
6
+ test business behavior through core's `runCapability()` / `simulateFlow()` and run
7
+ `plumbus test`; do not add separate Vitest or Zod dependencies.
8
+
9
+ For core 0.7.4+ classification, use `mockAI({ classify: ['billing'] })` and test
10
+ through `runCapability()` / `simulateFlow()`. Integration tests with a real
11
+ `createAIService({ decisions, onAICostRecorded })` should assert the
12
+ `openai-decisions` provider/model forwarding, threshold behavior, one `classify` cost
13
+ row, refusal handling (`error.refusedQuestions`), and unknown-cost handling.
14
+
15
+ For core 0.7.3+ `decide()` tests, configure `mockAI({ decide: result })` or a real
16
+ `createAIService({ decisions, costTracker, onAICostRecorded })` with injected provider
17
+ fetch. Assert exactly one cost row, identity, actual model/usage, billed validation
18
+ failures and refusals, and budget rejection before dispatch.
19
+
20
+ In the framework repository, run `pnpm lint`, `pnpm format:check`, `pnpm typecheck`,
21
+ and `pnpm test`. No credentials or network access are required.
22
+
23
+ For one live request, follow [Providing a live test environment](https://github.com/plumbus-framework/plumbus/blob/main/docs/ai/decision-providers.md#providing-a-live-test-environment).
24
+ Provide a private env-file path, not pasted secrets, containing
25
+ `PLUMBUS_LIVE_DECISION_TESTS=1` and `OPENAI_API_KEY` (optionally `OPENAI_DECISION_MODEL`
26
+ and `OPENAI_BASE_URL`). After building, run `scripts/smoke.mjs` from the package
27
+ directory. It sends one fixed, non-sensitive ticket with three questions. The script
28
+ is a repository tool, not an installed CLI command.
29
+
30
+ Measure live accuracy and calibration separately from connection success. Do not
31
+ assert a universal confidence cutoff or a guaranteed live label.
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@plumbus/ai-decision-openai",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "OpenAI Decisions API typed decision provider adapter for Plumbus.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "instructions",
17
+ "README.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "keywords": [
21
+ "plumbus",
22
+ "ai",
23
+ "decision",
24
+ "openai"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/plumbus-framework/plumbus.git",
30
+ "directory": "packages/ai-decision-openai"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "node": ">=20.6.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@plumbus/core": "0.7.x"
40
+ },
41
+ "devDependencies": {
42
+ "@biomejs/biome": "^2.4.6",
43
+ "@types/node": "^25.4.0",
44
+ "typescript": "^5.7.0",
45
+ "vitest": "^3.2.4",
46
+ "@plumbus/core": "0.7.9"
47
+ },
48
+ "dependencies": {
49
+ "@plumbus/ai-decision": "~0.2.3"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -b",
53
+ "dev": "tsc -b --watch",
54
+ "test": "vitest run",
55
+ "lint": "biome lint ./src",
56
+ "format": "biome format --write ./src",
57
+ "format:check": "biome format ./src",
58
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
59
+ "smoke": "pnpm build && node scripts/smoke.mjs"
60
+ }
6
61
  }