@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 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.0",
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 ? ['It must match this JSON schema:', JSON.stringify(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 = validate(req.schema, extractJson(out.text));
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 = validate(req.schema, extractJson(out.text));
191
+ object = read(out.text);
191
192
  } catch (second) {
192
193
  throw new ProviderError(provider.name, second.message, { kind: 'output' });
193
194
  }