@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 +1 -1
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/c0mpute.js +3 -2
- package/src/index.js +3 -2
- package/src/json.js +31 -1
- package/src/router.js +4 -3
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
|
|
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.
|
|
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
|
-
*
|
|
9
|
-
* trusted-client key (`ifc_
|
|
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
|
|
30
|
-
*
|
|
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
|
|
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
|
}
|