@profullstack/ai-providers 0.1.0 → 0.1.1
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/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/json.js +31 -1
- package/src/router.js +4 -3
package/index.d.ts
CHANGED
|
@@ -179,6 +179,7 @@ export declare class AnthropicProvider implements TextProvider {
|
|
|
179
179
|
|
|
180
180
|
export declare function extractJson(text: string): unknown;
|
|
181
181
|
export declare function validate<T>(schema: SchemaLike<T> | undefined, value: unknown): T;
|
|
182
|
+
export declare function unwrapEcho(value: unknown, jsonSchema?: Record<string, unknown>, name?: string): unknown;
|
|
182
183
|
export declare function jsonInstructions(name?: string, jsonSchema?: Record<string, unknown>): string;
|
|
183
184
|
|
|
184
185
|
export interface CreateOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/ai-providers",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One text-model call for every Profullstack app: c0mpute first, OpenAI and Anthropic as alternating fallbacks, dead keys parked. Zero dependencies.",
|
|
6
6
|
"keywords": [
|
package/src/json.js
CHANGED
|
@@ -57,9 +57,39 @@ export function validate(schema, value) {
|
|
|
57
57
|
return value;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Small models shown a JSON Schema often answer in the shape of the schema, with
|
|
64
|
+
* correct values one level down: {"$schema":..., "type":"object", "properties":
|
|
65
|
+
* {"headline":"Real copy", ...}, "required":[...]}. Nothing is wrong with the
|
|
66
|
+
* values, but validating that object reports every field as undefined. Same for
|
|
67
|
+
* {"ad_copy":{...}}, where the object is wrapped in its own name.
|
|
68
|
+
*
|
|
69
|
+
* Only unwraps when the top level has none of the schema's fields and the inner
|
|
70
|
+
* object has at least one, so a schema with a field really called `properties`
|
|
71
|
+
* is left alone.
|
|
72
|
+
*/
|
|
73
|
+
export function unwrapEcho(value, jsonSchema, name) {
|
|
74
|
+
const keys = Object.keys(jsonSchema?.properties ?? {});
|
|
75
|
+
if (!keys.length || !isObject(value) || keys.some((k) => k in value)) return value;
|
|
76
|
+
const entries = Object.values(value);
|
|
77
|
+
const candidates = [value.properties, name ? value[name] : undefined, entries.length === 1 ? entries[0] : undefined];
|
|
78
|
+
return candidates.find((c) => isObject(c) && keys.some((k) => k in c)) ?? value;
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
export function jsonInstructions(name, jsonSchema) {
|
|
82
|
+
// The meta keys add nothing to the answer and are what start a model copying
|
|
83
|
+
// the schema instead of filling it in.
|
|
84
|
+
const { $schema, $id, ...shape } = jsonSchema ?? {};
|
|
61
85
|
return [
|
|
62
86
|
`Return only one JSON object${name ? ` (${name})` : ''}. No Markdown, no code fences, no commentary.`,
|
|
63
|
-
...(jsonSchema
|
|
87
|
+
...(jsonSchema
|
|
88
|
+
? [
|
|
89
|
+
'It must match this JSON schema:',
|
|
90
|
+
JSON.stringify(shape),
|
|
91
|
+
'The schema describes your answer, it is not part of it. Put each value at the top level of the object, and never write the words "type", "properties" or "required" as keys.',
|
|
92
|
+
]
|
|
93
|
+
: []),
|
|
64
94
|
].join('\n');
|
|
65
95
|
}
|
package/src/router.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* A success clears it.
|
|
14
14
|
*/
|
|
15
15
|
import { ProviderError, toProviderError } from './errors.js';
|
|
16
|
-
import { extractJson, validate, jsonInstructions } from './json.js';
|
|
16
|
+
import { extractJson, validate, jsonInstructions, unwrapEcho } from './json.js';
|
|
17
17
|
|
|
18
18
|
export class AllProvidersFailedError extends Error {
|
|
19
19
|
constructor(attempts, skipped = []) {
|
|
@@ -163,10 +163,11 @@ export class AiRouter {
|
|
|
163
163
|
export async function generateObjectOn(provider, req) {
|
|
164
164
|
const system = [req.system, jsonInstructions(req.name, req.jsonSchema)].filter(Boolean).join('\n\n');
|
|
165
165
|
const base = { ...req, system, json: true };
|
|
166
|
+
const read = (text) => validate(req.schema, unwrapEcho(extractJson(text), req.jsonSchema, req.name));
|
|
166
167
|
let out = await provider.generate(base);
|
|
167
168
|
let object;
|
|
168
169
|
try {
|
|
169
|
-
object =
|
|
170
|
+
object = read(out.text);
|
|
170
171
|
} catch (first) {
|
|
171
172
|
const repairs = req.repairs ?? 1;
|
|
172
173
|
if (repairs < 1) throw new ProviderError(provider.name, first.message, { kind: 'output' });
|
|
@@ -187,7 +188,7 @@ export async function generateObjectOn(provider, req) {
|
|
|
187
188
|
].join('\n'),
|
|
188
189
|
});
|
|
189
190
|
try {
|
|
190
|
-
object =
|
|
191
|
+
object = read(out.text);
|
|
191
192
|
} catch (second) {
|
|
192
193
|
throw new ProviderError(provider.name, second.message, { kind: 'output' });
|
|
193
194
|
}
|