@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.
- package/CHANGELOG.md +12 -0
- package/README.md +219 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/openai-adapter.d.ts +20 -0
- package/dist/openai-adapter.d.ts.map +1 -0
- package/dist/openai-adapter.js +246 -0
- package/dist/openai-adapter.js.map +1 -0
- package/instructions/README.md +22 -0
- package/instructions/framework.md +99 -0
- package/instructions/testing.md +31 -0
- package/package.json +58 -3
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|