@profullstack/ai-providers 0.1.0 → 0.1.2

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
@@ -57,7 +57,7 @@ A thrown error with `status` 401/402/403, or a 429 or 400 that mentions credits
57
57
 
58
58
  | Variable | Default | |
59
59
  |---|---|---|
60
- | `C0MPUTE_API_KEY` | none | Infernet trusted-client key (`ifc_...`), one per app. Without it the public limit is 20 requests an hour per IP. |
60
+ | `C0MPUTE_API_KEY` | none | Infernet **account** key (`ik_live_...`), billed per job. An `ifc_` trusted-client key without an end-user subject, or no key at all, gets the free tier of 10 queries a day. |
61
61
  | `C0MPUTE_URL` | `https://infernetprotocol.com` | |
62
62
  | `C0MPUTE_MODELS` | `huihui_ai/qwen3.5-abliterated:9b,qwen2.5:7b,qwen3:4b,gemma3:4b,huihui_ai/qwen3.5-abliterated:4b` | Tried in order. The first is an Infernet house model every node pulls. |
63
63
  | `C0MPUTE_ANY_MODEL` | off | Let any free node answer as a last resort. Small models write junk JSON. |
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.2",
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/c0mpute.js CHANGED
@@ -5,8 +5,9 @@
5
5
  * -> {jobId, source, model}
6
6
  * GET /api/chat/stream/<jobId> server-sent events: meta, token..., done | error
7
7
  *
8
- * Without a key the public playground allows 20 requests an hour per IP. A
9
- * trusted-client key (`ifc_...`, one per app) has its own budget.
8
+ * Use an account key (`ik_live_...`): it is billed per job with no daily cap.
9
+ * With no key, or a trusted-client key (`ifc_...`) that names no end user, the
10
+ * caller gets the free tier of 10 queries a day.
10
11
  *
11
12
  * `p2p_only` (default on) stops Infernet handing the job to its billed hosted
12
13
  * fallback; an answer from anything but a peer node is refused.
package/src/index.js CHANGED
@@ -26,8 +26,9 @@ const num = (v) => (v == null || v === '' || Number.isNaN(Number(v)) ? undefined
26
26
  /**
27
27
  * Builds the router from the environment.
28
28
  *
29
- * C0MPUTE_API_KEY Infernet trusted-client key (ifc_...). Without it the
30
- * public limit of 20 requests an hour per IP applies.
29
+ * C0MPUTE_API_KEY Infernet account key (ik_live_...), billed per job. No
30
+ * key, or an ifc_ key with no subject, gets 10 free
31
+ * queries a day.
31
32
  * C0MPUTE_URL default https://infernetprotocol.com
32
33
  * C0MPUTE_MODELS comma list, tried in order (C0MPUTE_MODEL also read)
33
34
  * C0MPUTE_ANY_MODEL 1 to let any free node answer as a last resort
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
  }