@pawabase/client 0.1.3 → 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/README.md +26 -14
- package/dist/cjs/admin.d.ts +70 -0
- package/dist/cjs/admin.d.ts.map +1 -1
- package/dist/cjs/admin.js +75 -1
- package/dist/cjs/admin.js.map +1 -1
- package/dist/cjs/agents.d.ts +87 -0
- package/dist/cjs/agents.d.ts.map +1 -0
- package/dist/cjs/agents.js +153 -0
- package/dist/cjs/agents.js.map +1 -0
- package/dist/cjs/ai.d.ts +78 -0
- package/dist/cjs/ai.d.ts.map +1 -0
- package/dist/cjs/ai.js +127 -0
- package/dist/cjs/ai.js.map +1 -0
- package/dist/cjs/auth.d.ts +38 -0
- package/dist/cjs/auth.d.ts.map +1 -1
- package/dist/cjs/auth.js +52 -0
- package/dist/cjs/auth.js.map +1 -1
- package/dist/cjs/client.d.ts +18 -3
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +30 -3
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/data.d.ts +28 -0
- package/dist/cjs/data.d.ts.map +1 -1
- package/dist/cjs/data.js +42 -7
- package/dist/cjs/data.js.map +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.d.ts.map +1 -1
- package/dist/cjs/errors.js +8 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/functions.d.ts +0 -28
- package/dist/cjs/functions.d.ts.map +1 -1
- package/dist/cjs/functions.js +1 -42
- package/dist/cjs/functions.js.map +1 -1
- package/dist/cjs/index.d.ts +6 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +6 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/manage/access.d.ts +4 -62
- package/dist/cjs/manage/access.d.ts.map +1 -1
- package/dist/cjs/manage/access.js +4 -85
- package/dist/cjs/manage/access.js.map +1 -1
- package/dist/cjs/manage/agents.d.ts +76 -0
- package/dist/cjs/manage/agents.d.ts.map +1 -0
- package/dist/cjs/manage/agents.js +89 -0
- package/dist/cjs/manage/agents.js.map +1 -0
- package/dist/cjs/manage/ai.d.ts +90 -0
- package/dist/cjs/manage/ai.d.ts.map +1 -0
- package/dist/cjs/manage/ai.js +101 -0
- package/dist/cjs/manage/ai.js.map +1 -0
- package/dist/cjs/manage/base.d.ts +6 -16
- package/dist/cjs/manage/base.d.ts.map +1 -1
- package/dist/cjs/manage/base.js +11 -11
- package/dist/cjs/manage/base.js.map +1 -1
- package/dist/cjs/manage/data.d.ts +14 -16
- package/dist/cjs/manage/data.d.ts.map +1 -1
- package/dist/cjs/manage/data.js +15 -22
- package/dist/cjs/manage/data.js.map +1 -1
- package/dist/cjs/manage/files.d.ts +5 -0
- package/dist/cjs/manage/files.d.ts.map +1 -1
- package/dist/cjs/manage/files.js +10 -1
- package/dist/cjs/manage/files.js.map +1 -1
- package/dist/cjs/manage/index.d.ts +11 -10
- package/dist/cjs/manage/index.d.ts.map +1 -1
- package/dist/cjs/manage/index.js +15 -12
- package/dist/cjs/manage/index.js.map +1 -1
- package/dist/cjs/manage/lifecycle.d.ts +15 -38
- package/dist/cjs/manage/lifecycle.d.ts.map +1 -1
- package/dist/cjs/manage/lifecycle.js +21 -52
- package/dist/cjs/manage/lifecycle.js.map +1 -1
- package/dist/cjs/manage/mail.d.ts +12 -2
- package/dist/cjs/manage/mail.d.ts.map +1 -1
- package/dist/cjs/manage/mail.js +12 -1
- package/dist/cjs/manage/mail.js.map +1 -1
- package/dist/cjs/manage/observe.d.ts +6 -0
- package/dist/cjs/manage/observe.d.ts.map +1 -1
- package/dist/cjs/manage/observe.js +8 -2
- package/dist/cjs/manage/observe.js.map +1 -1
- package/dist/cjs/types.d.ts +3 -3
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/admin.d.ts +70 -0
- package/dist/esm/admin.d.ts.map +1 -1
- package/dist/esm/admin.js +72 -0
- package/dist/esm/admin.js.map +1 -1
- package/dist/esm/agents.d.ts +87 -0
- package/dist/esm/agents.d.ts.map +1 -0
- package/dist/esm/agents.js +148 -0
- package/dist/esm/agents.js.map +1 -0
- package/dist/esm/ai.d.ts +78 -0
- package/dist/esm/ai.d.ts.map +1 -0
- package/dist/esm/ai.js +123 -0
- package/dist/esm/ai.js.map +1 -0
- package/dist/esm/auth.d.ts +38 -0
- package/dist/esm/auth.d.ts.map +1 -1
- package/dist/esm/auth.js +52 -0
- package/dist/esm/auth.js.map +1 -1
- package/dist/esm/client.d.ts +18 -3
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +31 -4
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/data.d.ts +28 -0
- package/dist/esm/data.d.ts.map +1 -1
- package/dist/esm/data.js +42 -7
- package/dist/esm/data.js.map +1 -1
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.d.ts.map +1 -1
- package/dist/esm/errors.js +8 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/functions.d.ts +0 -28
- package/dist/esm/functions.d.ts.map +1 -1
- package/dist/esm/functions.js +0 -40
- package/dist/esm/functions.js.map +1 -1
- package/dist/esm/index.d.ts +6 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/manage/access.d.ts +4 -62
- package/dist/esm/manage/access.d.ts.map +1 -1
- package/dist/esm/manage/access.js +3 -83
- package/dist/esm/manage/access.js.map +1 -1
- package/dist/esm/manage/agents.d.ts +76 -0
- package/dist/esm/manage/agents.d.ts.map +1 -0
- package/dist/esm/manage/agents.js +82 -0
- package/dist/esm/manage/agents.js.map +1 -0
- package/dist/esm/manage/ai.d.ts +90 -0
- package/dist/esm/manage/ai.d.ts.map +1 -0
- package/dist/esm/manage/ai.js +97 -0
- package/dist/esm/manage/ai.js.map +1 -0
- package/dist/esm/manage/base.d.ts +6 -16
- package/dist/esm/manage/base.d.ts.map +1 -1
- package/dist/esm/manage/base.js +11 -11
- package/dist/esm/manage/base.js.map +1 -1
- package/dist/esm/manage/data.d.ts +14 -16
- package/dist/esm/manage/data.d.ts.map +1 -1
- package/dist/esm/manage/data.js +14 -19
- package/dist/esm/manage/data.js.map +1 -1
- package/dist/esm/manage/files.d.ts +5 -0
- package/dist/esm/manage/files.d.ts.map +1 -1
- package/dist/esm/manage/files.js +10 -1
- package/dist/esm/manage/files.js.map +1 -1
- package/dist/esm/manage/index.d.ts +11 -10
- package/dist/esm/manage/index.d.ts.map +1 -1
- package/dist/esm/manage/index.js +18 -15
- package/dist/esm/manage/index.js.map +1 -1
- package/dist/esm/manage/lifecycle.d.ts +15 -38
- package/dist/esm/manage/lifecycle.d.ts.map +1 -1
- package/dist/esm/manage/lifecycle.js +19 -50
- package/dist/esm/manage/lifecycle.js.map +1 -1
- package/dist/esm/manage/mail.d.ts +12 -2
- package/dist/esm/manage/mail.d.ts.map +1 -1
- package/dist/esm/manage/mail.js +13 -2
- package/dist/esm/manage/mail.js.map +1 -1
- package/dist/esm/manage/observe.d.ts +6 -0
- package/dist/esm/manage/observe.d.ts.map +1 -1
- package/dist/esm/manage/observe.js +8 -2
- package/dist/esm/manage/observe.js.map +1 -1
- package/dist/esm/types.d.ts +3 -3
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
- package/src/admin.ts +104 -0
- package/src/agents.ts +184 -0
- package/src/ai.ts +164 -0
- package/src/auth.ts +70 -0
- package/src/client.ts +31 -4
- package/src/data.ts +52 -5
- package/src/errors.ts +6 -1
- package/src/functions.ts +0 -49
- package/src/index.ts +6 -1
- package/src/manage/access.ts +3 -96
- package/src/manage/agents.ts +98 -0
- package/src/manage/ai.ts +129 -0
- package/src/manage/base.ts +11 -11
- package/src/manage/data.ts +15 -20
- package/src/manage/files.ts +12 -1
- package/src/manage/index.ts +18 -15
- package/src/manage/lifecycle.ts +20 -64
- package/src/manage/mail.ts +16 -2
- package/src/manage/observe.ts +9 -2
- package/src/types.ts +3 -3
- package/src/version.ts +1 -1
- package/dist/cjs/manage/functions.d.ts +0 -48
- package/dist/cjs/manage/functions.d.ts.map +0 -1
- package/dist/cjs/manage/functions.js +0 -61
- package/dist/cjs/manage/functions.js.map +0 -1
- package/dist/esm/manage/functions.d.ts +0 -48
- package/dist/esm/manage/functions.d.ts.map +0 -1
- package/dist/esm/manage/functions.js +0 -57
- package/dist/esm/manage/functions.js.map +0 -1
- package/src/manage/functions.ts +0 -69
package/src/agents.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents and knowledge bases, run from your app: the signed-in user's access rules decide which agents they may run and which
|
|
3
|
+
* knowledge bases they may search.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const run = await pawabase.ai.agents.run("support", "How do I reset my password?");
|
|
7
|
+
* console.log(run.output);
|
|
8
|
+
* for await (const event of pawabase.ai.agents.stream("support", "Summarise my open tickets")) console.log(event);
|
|
9
|
+
* const hits = await pawabase.ai.knowledge.search("docs", "reset password");
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { ApiError } from "./errors.js";
|
|
13
|
+
import type { HttpClient } from "./http.js";
|
|
14
|
+
|
|
15
|
+
export type AgentRunStatus = "running" | "waiting" | "finished" | "failed" | "cancelled" | (string & {});
|
|
16
|
+
|
|
17
|
+
/** One run of an agent. A run that stopped to ask permission has `status: "waiting"` and lists what it is waiting on. */
|
|
18
|
+
export interface AgentRun {
|
|
19
|
+
id: string;
|
|
20
|
+
agent: string;
|
|
21
|
+
status: AgentRunStatus;
|
|
22
|
+
/** The agent's answer, once it has one. */
|
|
23
|
+
output?: string;
|
|
24
|
+
thread?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
[field: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One step of a streamed run (`type` says which: text pieces, tool calls and results, approvals, `error`). */
|
|
30
|
+
export interface AgentEvent {
|
|
31
|
+
type: string;
|
|
32
|
+
[field: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AgentRunOptions {
|
|
36
|
+
/** Carry on a conversation: the `thread` of an earlier run. */
|
|
37
|
+
thread?: string;
|
|
38
|
+
/** Return at once; poll `get()` or start a flow from `agent.run.finished`. */
|
|
39
|
+
background?: boolean;
|
|
40
|
+
signal?: AbortSignal | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface KnowledgeHit {
|
|
44
|
+
[field: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface KnowledgeDocument {
|
|
48
|
+
id: string;
|
|
49
|
+
status: "pending" | "ready" | "failed" | (string & {});
|
|
50
|
+
title?: string;
|
|
51
|
+
error?: string;
|
|
52
|
+
chunks?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const seg = (...parts: string[]) => "/" + parts.map(encodeURIComponent).join("/");
|
|
56
|
+
|
|
57
|
+
export class AgentsClient {
|
|
58
|
+
constructor(private readonly http: HttpClient) {}
|
|
59
|
+
|
|
60
|
+
/** The agents the caller may run. */
|
|
61
|
+
async list(): Promise<Array<{ name: string; description: string }>> {
|
|
62
|
+
const body = await this.http.request<{ data: Array<{ name: string; description: string }> }>({ path: "/ai/v1/agents" });
|
|
63
|
+
return body.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Send `input` and wait for the run (or, with `background`, only for it to start). */
|
|
67
|
+
run(name: string, input: string, options: AgentRunOptions = {}): Promise<AgentRun> {
|
|
68
|
+
return this.http.request<AgentRun>({
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: `/ai/v1${seg("agents", name, "runs")}`,
|
|
71
|
+
body: { input, thread: options.thread, background: options.background ?? false },
|
|
72
|
+
signal: options.signal ?? undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Each step of a run as it happens. */
|
|
77
|
+
async *stream(name: string, input: string, options: Omit<AgentRunOptions, "background"> = {}): AsyncGenerator<AgentEvent, void, void> {
|
|
78
|
+
const stream = await this.http.request<ReadableStream<Uint8Array> | null>({
|
|
79
|
+
method: "POST",
|
|
80
|
+
path: `/ai/v1${seg("agents", name, "runs")}`,
|
|
81
|
+
body: { input, thread: options.thread, stream: true },
|
|
82
|
+
parse: "stream",
|
|
83
|
+
signal: options.signal ?? undefined,
|
|
84
|
+
});
|
|
85
|
+
if (!stream) return;
|
|
86
|
+
const reader = stream.getReader();
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = "";
|
|
89
|
+
try {
|
|
90
|
+
for (;;) {
|
|
91
|
+
const { value, done } = await reader.read();
|
|
92
|
+
if (done) break;
|
|
93
|
+
buffer += decoder.decode(value, { stream: true });
|
|
94
|
+
let end: number;
|
|
95
|
+
while ((end = buffer.indexOf("\n\n")) >= 0) {
|
|
96
|
+
const event = buffer.slice(0, end);
|
|
97
|
+
buffer = buffer.slice(end + 2);
|
|
98
|
+
for (const line of event.split("\n")) {
|
|
99
|
+
if (!line.startsWith("data:")) continue;
|
|
100
|
+
const data = line.slice(5).trim();
|
|
101
|
+
if (data === "[DONE]") return;
|
|
102
|
+
let parsed: AgentEvent;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(data);
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (parsed.type === "error") {
|
|
109
|
+
throw new ApiError({ status: 502, code: "agent_error", message: String(parsed.message ?? "the run stopped") });
|
|
110
|
+
}
|
|
111
|
+
yield parsed;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
reader.releaseLock();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A run of yours: its status and answer, and what it is waiting on if it stopped to ask. */
|
|
121
|
+
get(runId: string): Promise<AgentRun> {
|
|
122
|
+
return this.http.request<AgentRun>({ path: `/ai/v1${seg("agent-runs", runId)}` });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Approve or decline what a run is waiting on: a decision per call id, and `approve` for any not named. */
|
|
126
|
+
decide(runId: string, options: { decisions?: Record<string, boolean>; approve?: boolean; background?: boolean } = {}): Promise<AgentRun> {
|
|
127
|
+
return this.http.request<AgentRun>({
|
|
128
|
+
method: "POST",
|
|
129
|
+
path: `/ai/v1${seg("agent-runs", runId, "decide")}`,
|
|
130
|
+
body: { decisions: options.decisions ?? {}, approve: options.approve, background: options.background ?? false },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Poll a background run until it is no longer running (a waiting run is returned too, so you can decide it). */
|
|
135
|
+
async wait(runId: string, options: { intervalMs?: number; timeoutMs?: number; signal?: AbortSignal | null } = {}): Promise<AgentRun> {
|
|
136
|
+
const interval = options.intervalMs ?? 1000;
|
|
137
|
+
const deadline = Date.now() + (options.timeoutMs ?? 5 * 60_000);
|
|
138
|
+
for (;;) {
|
|
139
|
+
const run = await this.get(runId);
|
|
140
|
+
if (run.status !== "running") return run;
|
|
141
|
+
if (options.signal?.aborted) throw options.signal.reason ?? new Error("aborted");
|
|
142
|
+
if (Date.now() >= deadline) throw new Error(`run ${runId} was still running after ${options.timeoutMs ?? 5 * 60_000} ms`);
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class KnowledgeClient {
|
|
149
|
+
constructor(private readonly http: HttpClient) {}
|
|
150
|
+
|
|
151
|
+
/** Search by meaning and by words together. Documents kept private to someone else are left out. */
|
|
152
|
+
async search(name: string, query: string, options: { limit?: number; signal?: AbortSignal | null } = {}): Promise<KnowledgeHit[]> {
|
|
153
|
+
const body = await this.http.request<{ data: KnowledgeHit[] }>({
|
|
154
|
+
method: "POST",
|
|
155
|
+
path: `/ai/v1${seg("knowledge", name, "search")}`,
|
|
156
|
+
body: { query, limit: options.limit ?? 5 },
|
|
157
|
+
signal: options.signal ?? undefined,
|
|
158
|
+
});
|
|
159
|
+
return body.data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Add text, or a web page to read (`url`). A signed-in user's documents are found only by them. Adding the same `source` again replaces it. */
|
|
163
|
+
addDocument(name: string, input: { text?: string; url?: string; title?: string; source?: string }): Promise<KnowledgeDocument> {
|
|
164
|
+
return this.http.request<KnowledgeDocument>({
|
|
165
|
+
method: "POST",
|
|
166
|
+
path: `/ai/v1${seg("knowledge", name, "documents")}`,
|
|
167
|
+
body: { title: input.title ?? "", source: input.source ?? "", text: input.text, url: input.url },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Add a file (PDF, Word, HTML or text). */
|
|
172
|
+
addFile(name: string, file: Blob, options: { title?: string; source?: string; filename?: string } = {}): Promise<KnowledgeDocument> {
|
|
173
|
+
const form = new FormData();
|
|
174
|
+
form.append("file", file, options.filename ?? (file as File).name ?? "document");
|
|
175
|
+
if (options.title) form.append("title", options.title);
|
|
176
|
+
if (options.source) form.append("source", options.source);
|
|
177
|
+
return this.http.request<KnowledgeDocument>({ method: "POST", path: `/ai/v1${seg("knowledge", name, "documents")}`, body: form });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Whether a document is ready to search, and why not if it failed. */
|
|
181
|
+
document(name: string, documentId: string): Promise<KnowledgeDocument> {
|
|
182
|
+
return this.http.request<KnowledgeDocument>({ path: `/ai/v1${seg("knowledge", name, "documents", documentId)}` });
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/ai.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI gateway: chat and embedding models your environment has connected, behind one OpenAI-compatible API.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* const answer = await pawabase.ai.chat("chat", "Summarise this ticket: …");
|
|
6
|
+
* for await (const piece of pawabase.ai.stream("chat", "Tell me a story")) process.stdout.write(piece);
|
|
7
|
+
* const vector = await pawabase.ai.embed("embed", "hello");
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Models are called by the name your environment gave them. Who may call each is the model's policy; servers (a secret key) always may.
|
|
11
|
+
*/
|
|
12
|
+
import { ApiError } from "./errors.js";
|
|
13
|
+
import { AgentsClient, KnowledgeClient } from "./agents.js";
|
|
14
|
+
import type { HttpClient } from "./http.js";
|
|
15
|
+
|
|
16
|
+
export interface ChatMessage {
|
|
17
|
+
role: "system" | "user" | "assistant" | "tool" | "developer";
|
|
18
|
+
content: string | null;
|
|
19
|
+
tool_calls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }>;
|
|
20
|
+
tool_call_id?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ChatOptions {
|
|
25
|
+
temperature?: number;
|
|
26
|
+
topP?: number;
|
|
27
|
+
maxTokens?: number;
|
|
28
|
+
stop?: string | string[];
|
|
29
|
+
/** Functions the model may ask you to call (OpenAI's `tools` format). */
|
|
30
|
+
tools?: unknown[];
|
|
31
|
+
toolChoice?: unknown;
|
|
32
|
+
/** `{ type: "json_object" }` asks for JSON back, where the provider supports it. */
|
|
33
|
+
responseFormat?: { type: string; [key: string]: unknown };
|
|
34
|
+
/** Any other parameter, passed through as given. */
|
|
35
|
+
extra?: Record<string, unknown>;
|
|
36
|
+
signal?: AbortSignal | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ChatCompletion {
|
|
40
|
+
id: string;
|
|
41
|
+
object: "chat.completion";
|
|
42
|
+
model: string;
|
|
43
|
+
choices: Array<{ index: number; message: ChatMessage; finish_reason: string | null }>;
|
|
44
|
+
usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AiModelInfo {
|
|
48
|
+
id: string;
|
|
49
|
+
kind: "chat" | "embedding";
|
|
50
|
+
description: string;
|
|
51
|
+
dimensions?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function asMessages(input: string | ChatMessage[]): ChatMessage[] {
|
|
55
|
+
return typeof input === "string" ? [{ role: "user", content: input }] : input;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bodyOf(model: string, input: string | ChatMessage[], options: ChatOptions): Record<string, unknown> {
|
|
59
|
+
return {
|
|
60
|
+
...(options.extra ?? {}),
|
|
61
|
+
model,
|
|
62
|
+
messages: asMessages(input),
|
|
63
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
64
|
+
...(options.topP !== undefined ? { top_p: options.topP } : {}),
|
|
65
|
+
...(options.maxTokens !== undefined ? { max_tokens: options.maxTokens } : {}),
|
|
66
|
+
...(options.stop !== undefined ? { stop: options.stop } : {}),
|
|
67
|
+
...(options.tools !== undefined ? { tools: options.tools } : {}),
|
|
68
|
+
...(options.toolChoice !== undefined ? { tool_choice: options.toolChoice } : {}),
|
|
69
|
+
...(options.responseFormat !== undefined ? { response_format: options.responseFormat } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class AiClient {
|
|
74
|
+
/** Agents the caller may run: `run`, `stream`, `get`, `decide`, `wait`. */
|
|
75
|
+
readonly agents: AgentsClient;
|
|
76
|
+
/** Knowledge bases the caller may search or add documents to. */
|
|
77
|
+
readonly knowledge: KnowledgeClient;
|
|
78
|
+
|
|
79
|
+
constructor(private readonly http: HttpClient) {
|
|
80
|
+
this.agents = new AgentsClient(http);
|
|
81
|
+
this.knowledge = new KnowledgeClient(http);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The models the caller may use. */
|
|
85
|
+
async models(): Promise<AiModelInfo[]> {
|
|
86
|
+
const body = await this.http.request<{ data: AiModelInfo[] }>({ path: "/ai/v1/models" });
|
|
87
|
+
return body.data;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The full chat completion, in OpenAI's shape (tool calls, usage and all). */
|
|
91
|
+
complete(model: string, input: string | ChatMessage[], options: ChatOptions = {}): Promise<ChatCompletion> {
|
|
92
|
+
return this.http.request<ChatCompletion>({
|
|
93
|
+
method: "POST",
|
|
94
|
+
path: "/ai/v1/chat/completions",
|
|
95
|
+
body: bodyOf(model, input, options),
|
|
96
|
+
signal: options.signal ?? undefined,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Ask a model and get its answer as text. */
|
|
101
|
+
async chat(model: string, input: string | ChatMessage[], options: ChatOptions = {}): Promise<string> {
|
|
102
|
+
const completion = await this.complete(model, input, options);
|
|
103
|
+
return completion.choices[0]?.message?.content ?? "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The answer as it is written: an async iterator of text pieces. */
|
|
107
|
+
async *stream(model: string, input: string | ChatMessage[], options: ChatOptions = {}): AsyncGenerator<string, void, void> {
|
|
108
|
+
const stream = await this.http.request<ReadableStream<Uint8Array> | null>({
|
|
109
|
+
method: "POST",
|
|
110
|
+
path: "/ai/v1/chat/completions",
|
|
111
|
+
body: { ...bodyOf(model, input, options), stream: true },
|
|
112
|
+
parse: "stream",
|
|
113
|
+
signal: options.signal ?? undefined,
|
|
114
|
+
});
|
|
115
|
+
if (!stream) return;
|
|
116
|
+
const reader = stream.getReader();
|
|
117
|
+
const decoder = new TextDecoder();
|
|
118
|
+
let buffer = "";
|
|
119
|
+
try {
|
|
120
|
+
for (;;) {
|
|
121
|
+
const { value, done } = await reader.read();
|
|
122
|
+
if (done) break;
|
|
123
|
+
buffer += decoder.decode(value, { stream: true });
|
|
124
|
+
let end: number;
|
|
125
|
+
while ((end = buffer.indexOf("\n\n")) >= 0) {
|
|
126
|
+
const event = buffer.slice(0, end);
|
|
127
|
+
buffer = buffer.slice(end + 2);
|
|
128
|
+
for (const line of event.split("\n")) {
|
|
129
|
+
if (!line.startsWith("data:")) continue;
|
|
130
|
+
const data = line.slice(5).trim();
|
|
131
|
+
if (data === "[DONE]") return;
|
|
132
|
+
let parsed: { choices?: Array<{ delta?: { content?: string } }>; error?: { message?: string; code?: string } };
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(data);
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (parsed.error) {
|
|
139
|
+
throw new ApiError({ status: 502, code: parsed.error.code ?? "provider_error", message: parsed.error.message ?? "the model stopped answering" });
|
|
140
|
+
}
|
|
141
|
+
const piece = parsed.choices?.[0]?.delta?.content;
|
|
142
|
+
if (piece) yield piece;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
reader.releaseLock();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The vector for one text, or one vector per text when given a list. */
|
|
152
|
+
embed(model: string, input: string, options?: { signal?: AbortSignal | null }): Promise<number[]>;
|
|
153
|
+
embed(model: string, input: string[], options?: { signal?: AbortSignal | null }): Promise<number[][]>;
|
|
154
|
+
async embed(model: string, input: string | string[], options: { signal?: AbortSignal | null } = {}): Promise<number[] | number[][]> {
|
|
155
|
+
const body = await this.http.request<{ data: Array<{ index: number; embedding: number[] }> }>({
|
|
156
|
+
method: "POST",
|
|
157
|
+
path: "/ai/v1/embeddings",
|
|
158
|
+
body: { model, input },
|
|
159
|
+
signal: options.signal ?? undefined,
|
|
160
|
+
});
|
|
161
|
+
const vectors = [...body.data].sort((a, b) => a.index - b.index).map((row) => row.embedding);
|
|
162
|
+
return typeof input === "string" ? (vectors[0] ?? []) : vectors;
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/auth.ts
CHANGED
|
@@ -149,6 +149,8 @@ export class AuthClient implements TokenProvider {
|
|
|
149
149
|
readonly devices: AuthDevices;
|
|
150
150
|
/** Keys a user makes for themselves to call the API as themselves. */
|
|
151
151
|
readonly apiKeys: AuthApiKeys;
|
|
152
|
+
/** Sign a machine (a service or an AI agent) in with its client id and key. */
|
|
153
|
+
readonly machine: AuthMachine;
|
|
152
154
|
/** Passkeys (WebAuthn): register one, sign in with one. */
|
|
153
155
|
readonly passkeys: AuthPasskeys;
|
|
154
156
|
|
|
@@ -167,6 +169,7 @@ export class AuthClient implements TokenProvider {
|
|
|
167
169
|
this.orgs = new AuthOrgs(http);
|
|
168
170
|
this.devices = new AuthDevices(http);
|
|
169
171
|
this.apiKeys = new AuthApiKeys(http);
|
|
172
|
+
this.machine = new AuthMachine(http);
|
|
170
173
|
this.passkeys = new AuthPasskeys(http, (wire) => this.adopt(wire));
|
|
171
174
|
this.ready = this.initialize();
|
|
172
175
|
this.ready.catch(() => undefined);
|
|
@@ -747,6 +750,33 @@ export class AuthClient implements TokenProvider {
|
|
|
747
750
|
return user;
|
|
748
751
|
}
|
|
749
752
|
|
|
753
|
+
/**
|
|
754
|
+
* Set the signed-in user's picture from an image (PNG, JPEG, WebP or GIF). It is stored in the environment's public avatar
|
|
755
|
+
* bucket and `avatar_url` points at it; the previous picture is removed. Resolves the updated user.
|
|
756
|
+
*/
|
|
757
|
+
async setAvatar(image: Blob | ArrayBuffer | ArrayBufferView, options: { contentType?: string } = {}): Promise<User> {
|
|
758
|
+
await this.ready;
|
|
759
|
+
const declared = options.contentType ?? (typeof Blob !== "undefined" && image instanceof Blob && image.type ? image.type : "");
|
|
760
|
+
await this.http.request({
|
|
761
|
+
method: "PUT",
|
|
762
|
+
path: "/auth/v1/user/avatar",
|
|
763
|
+
body: image,
|
|
764
|
+
headers: { "content-type": declared || "application/octet-stream" },
|
|
765
|
+
});
|
|
766
|
+
const user = await this.getUser();
|
|
767
|
+
this.emit("USER_UPDATED");
|
|
768
|
+
return user;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Remove the signed-in user's picture. */
|
|
772
|
+
async removeAvatar(): Promise<User> {
|
|
773
|
+
await this.ready;
|
|
774
|
+
await this.http.request({ method: "DELETE", path: "/auth/v1/user/avatar" });
|
|
775
|
+
const user = await this.getUser();
|
|
776
|
+
this.emit("USER_UPDATED");
|
|
777
|
+
return user;
|
|
778
|
+
}
|
|
779
|
+
|
|
750
780
|
/** Send the confirmation email again. Always resolves the same way. */
|
|
751
781
|
async resendVerification(input: { email: string; redirectTo?: string }): Promise<void> {
|
|
752
782
|
await this.http.request({
|
|
@@ -1080,6 +1110,46 @@ class AuthDevices {
|
|
|
1080
1110
|
}
|
|
1081
1111
|
}
|
|
1082
1112
|
|
|
1113
|
+
/** What a machine gets for its credentials. */
|
|
1114
|
+
export interface MachineToken {
|
|
1115
|
+
accessToken: string;
|
|
1116
|
+
tokenType: "Bearer";
|
|
1117
|
+
/** Seconds the token lives. */
|
|
1118
|
+
expiresIn: number;
|
|
1119
|
+
/** The permissions the token carries, space-separated. */
|
|
1120
|
+
scope: string;
|
|
1121
|
+
machine: { id: string; name: string; kind: "service" | "agent" };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
class AuthMachine {
|
|
1125
|
+
constructor(private readonly http: HttpClient) {}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* The OAuth `client_credentials` grant: exchange a machine's id and one of its keys (`pbm_…`) for a short-lived access token.
|
|
1129
|
+
* `scope` (space-separated permissions) asks for fewer permissions than the key allows; it never grants more. Keeps no session.
|
|
1130
|
+
*/
|
|
1131
|
+
async token(input: { clientId: string; clientSecret: string; scope?: string }): Promise<MachineToken> {
|
|
1132
|
+
const wire = await this.http.request<{
|
|
1133
|
+
access_token: string;
|
|
1134
|
+
token_type: "Bearer";
|
|
1135
|
+
expires_in: number;
|
|
1136
|
+
scope: string;
|
|
1137
|
+
machine: MachineToken["machine"];
|
|
1138
|
+
}>({
|
|
1139
|
+
method: "POST",
|
|
1140
|
+
path: "/auth/v1/token",
|
|
1141
|
+
body: {
|
|
1142
|
+
grant_type: "client_credentials",
|
|
1143
|
+
client_id: input.clientId,
|
|
1144
|
+
client_secret: input.clientSecret,
|
|
1145
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
1146
|
+
},
|
|
1147
|
+
auth: "none",
|
|
1148
|
+
});
|
|
1149
|
+
return { accessToken: wire.access_token, tokenType: wire.token_type, expiresIn: wire.expires_in, scope: wire.scope, machine: wire.machine };
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1083
1153
|
class AuthApiKeys {
|
|
1084
1154
|
constructor(private readonly http: HttpClient) {}
|
|
1085
1155
|
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { AiClient } from "./ai.js";
|
|
1
2
|
import { AuthClient, type AuthOptions } from "./auth.js";
|
|
2
3
|
import { ResourceClient, type AnyRow, type ResourceTypes } from "./data.js";
|
|
3
4
|
import { ConfigError } from "./errors.js";
|
|
4
5
|
import { StorageClient } from "./files.js";
|
|
5
|
-
import { FlowsClient,
|
|
6
|
+
import { FlowsClient, RoutesClient } from "./functions.js";
|
|
6
7
|
import { HttpClient, type FetchLike, type Hooks, type RetryOptions, type TokenProvider } from "./http.js";
|
|
7
8
|
import { RealtimeClient, type RealtimeOptions } from "./realtime.js";
|
|
8
9
|
import { MemoryStorage } from "./session-store.js";
|
|
@@ -72,8 +73,8 @@ export class PawabaseClient<DB extends DatabaseShape = Untyped> {
|
|
|
72
73
|
readonly auth: AuthClient;
|
|
73
74
|
/** Storage buckets. */
|
|
74
75
|
readonly storage: StorageClient;
|
|
75
|
-
/**
|
|
76
|
-
readonly
|
|
76
|
+
/** The AI gateway: chat and embedding models. */
|
|
77
|
+
readonly ai: AiClient;
|
|
77
78
|
/** Flows with a pathless HTTP trigger: `/flows/v1`. */
|
|
78
79
|
readonly flows: FlowsClient;
|
|
79
80
|
/** Custom routes: `/rest/v1/<path>`. */
|
|
@@ -126,7 +127,7 @@ export class PawabaseClient<DB extends DatabaseShape = Untyped> {
|
|
|
126
127
|
});
|
|
127
128
|
config.tokens = stateless ? staticTokens(options.accessToken as NonNullable<ClientOptions["accessToken"]>) : this.auth;
|
|
128
129
|
this.storage = new StorageClient(this.http);
|
|
129
|
-
this.
|
|
130
|
+
this.ai = new AiClient(this.http);
|
|
130
131
|
this.flows = new FlowsClient(this.http);
|
|
131
132
|
this.routes = new RoutesClient(this.http);
|
|
132
133
|
this.realtime = new RealtimeClient(this.http, stateless ? null : this.auth, {
|
|
@@ -156,6 +157,32 @@ export class PawabaseClient<DB extends DatabaseShape = Untyped> {
|
|
|
156
157
|
return new PawabaseClient<DB>({ ...this.options, accessToken });
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
/**
|
|
161
|
+
* A client that acts as a machine (a service or an AI agent). It signs in with the machine's id and key, keeps the token until shortly
|
|
162
|
+
* before it expires, and signs in again when it must. It keeps no user session.
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* const agent = pawabase.asMachine({ clientId: process.env.PB_MACHINE_ID!, clientSecret: process.env.PB_MACHINE_KEY! });
|
|
166
|
+
* await agent.from("orders").eq("status", "open");
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
asMachine(input: { clientId: string; clientSecret: string; scope?: string }): PawabaseClient<DB> {
|
|
170
|
+
let cached: { token: string; until: number } | null = null;
|
|
171
|
+
let pending: Promise<string> | null = null;
|
|
172
|
+
const fetchToken = async (): Promise<string> => {
|
|
173
|
+
const got = await this.auth.machine.token(input);
|
|
174
|
+
cached = { token: got.accessToken, until: Date.now() + Math.max(got.expiresIn - 30, 1) * 1000 };
|
|
175
|
+
return got.accessToken;
|
|
176
|
+
};
|
|
177
|
+
return this.asUser(async () => {
|
|
178
|
+
if (cached && cached.until > Date.now()) return cached.token;
|
|
179
|
+
pending ??= fetchToken().finally(() => {
|
|
180
|
+
pending = null;
|
|
181
|
+
});
|
|
182
|
+
return pending;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
159
186
|
/**
|
|
160
187
|
* A raw request to any path on the gateway, with the key and the user's token attached,
|
|
161
188
|
* the retry policy and the typed errors applied. For endpoints the client has no method for.
|
package/src/data.ts
CHANGED
|
@@ -47,6 +47,8 @@ interface State {
|
|
|
47
47
|
expand: string[];
|
|
48
48
|
page: number;
|
|
49
49
|
perPage: number | null;
|
|
50
|
+
/** False asks for every match in one response. */
|
|
51
|
+
paginate: boolean;
|
|
50
52
|
/** An `in ()` with no values matches nothing: answered locally. */
|
|
51
53
|
empty: boolean;
|
|
52
54
|
}
|
|
@@ -121,6 +123,7 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
|
|
|
121
123
|
expand: [],
|
|
122
124
|
page: 1,
|
|
123
125
|
perPage: null,
|
|
126
|
+
paginate: true,
|
|
124
127
|
empty: false,
|
|
125
128
|
},
|
|
126
129
|
private readonly options: QueryOptions = {},
|
|
@@ -248,6 +251,14 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
|
|
|
248
251
|
return this.with({ perPage });
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Turn paging off with `paginate(false)`: the response holds every match (up to 5000; the server refuses more, so narrow
|
|
256
|
+
* with filters). `page` and `per_page` come back `null`.
|
|
257
|
+
*/
|
|
258
|
+
paginate(paginate = true): QueryBuilder<Row> {
|
|
259
|
+
return this.with({ paginate });
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
/** Alias of {@link perPage}. */
|
|
252
263
|
limit(count: number): QueryBuilder<Row> {
|
|
253
264
|
return this.perPage(count);
|
|
@@ -266,15 +277,20 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
|
|
|
266
277
|
if (this.state.sort.length) params["sort"] = this.state.sort.join(",");
|
|
267
278
|
if (this.state.select) params["select"] = this.state.select.join(",");
|
|
268
279
|
if (this.state.expand.length) params["expand"] = [...new Set(this.state.expand)].join(",");
|
|
269
|
-
if (this.state.
|
|
270
|
-
|
|
280
|
+
if (!this.state.paginate) params["paginate"] = "false";
|
|
281
|
+
else {
|
|
282
|
+
if (this.state.page !== 1) params["page"] = this.state.page;
|
|
283
|
+
if (this.state.perPage !== null) params["per_page"] = this.state.perPage;
|
|
284
|
+
}
|
|
271
285
|
return params;
|
|
272
286
|
}
|
|
273
287
|
|
|
274
288
|
/** Run the query and return one page. `await`ing the builder does the same. */
|
|
275
289
|
async execute(): Promise<Page<Row>> {
|
|
276
290
|
if (this.state.empty) {
|
|
277
|
-
return
|
|
291
|
+
return this.state.paginate
|
|
292
|
+
? { data: [], page: this.state.page, per_page: this.state.perPage ?? 20, total: 0 }
|
|
293
|
+
: { data: [], page: null, per_page: null, total: 0 };
|
|
278
294
|
}
|
|
279
295
|
return this.http.request<Page<Row>>({
|
|
280
296
|
path: `/rest/v1/${encodePath(this.resource)}`,
|
|
@@ -341,13 +357,13 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
|
|
|
341
357
|
seen += page.data.length;
|
|
342
358
|
if (options.maxRecords !== undefined && seen >= options.maxRecords) return;
|
|
343
359
|
if (page.total !== null) {
|
|
344
|
-
if (page.page * page.per_page >= page.total) return;
|
|
360
|
+
if ((page.page ?? 1) * (page.per_page ?? 0) >= page.total) return;
|
|
345
361
|
} else {
|
|
346
362
|
// Unknown total: a per-row policy filters after the page is read, so a short page proves nothing.
|
|
347
363
|
empties = page.data.length === 0 ? empties + 1 : 0;
|
|
348
364
|
if (empties >= stopAfterEmpty) return;
|
|
349
365
|
}
|
|
350
|
-
query = query.page(page.page + 1);
|
|
366
|
+
query = query.page((page.page ?? 1) + 1);
|
|
351
367
|
}
|
|
352
368
|
}
|
|
353
369
|
|
|
@@ -392,6 +408,34 @@ export class ResourceClient<
|
|
|
392
408
|
return id === undefined ? base : `${base}/${encodeURIComponent(String(id))}`;
|
|
393
409
|
}
|
|
394
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Records closest in meaning, closest first, each with a `distance` (smaller is closer). Give `text` (turned into a vector by the
|
|
413
|
+
* field's embedding model) or a `vector`. Vector fields are left out of the records unless you ask for them.
|
|
414
|
+
*/
|
|
415
|
+
async similar(input: {
|
|
416
|
+
text?: string;
|
|
417
|
+
vector?: number[];
|
|
418
|
+
field?: string;
|
|
419
|
+
metric?: "cosine" | "l2" | "ip";
|
|
420
|
+
limit?: number;
|
|
421
|
+
maxDistance?: number;
|
|
422
|
+
signal?: AbortSignal | null;
|
|
423
|
+
}): Promise<{ data: Array<Row & { distance: number }>; metric: string; count: number }> {
|
|
424
|
+
return this.http.request({
|
|
425
|
+
method: "POST",
|
|
426
|
+
path: `${this.path()}/similar`,
|
|
427
|
+
body: {
|
|
428
|
+
...(input.text !== undefined ? { text: input.text } : {}),
|
|
429
|
+
...(input.vector !== undefined ? { vector: input.vector } : {}),
|
|
430
|
+
...(input.field ? { field: input.field } : {}),
|
|
431
|
+
metric: input.metric ?? "cosine",
|
|
432
|
+
limit: input.limit ?? 10,
|
|
433
|
+
...(input.maxDistance !== undefined ? { max_distance: input.maxDistance } : {}),
|
|
434
|
+
},
|
|
435
|
+
signal: input.signal ?? undefined,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
395
439
|
/** Start a query. Every filter and shape method below is a shortcut for `query().…`. */
|
|
396
440
|
query(): QueryBuilder<Row> {
|
|
397
441
|
return new QueryBuilder<Row>(this.http, this.name);
|
|
@@ -405,6 +449,8 @@ export class ResourceClient<
|
|
|
405
449
|
expand?: string[];
|
|
406
450
|
page?: number;
|
|
407
451
|
perPage?: number;
|
|
452
|
+
/** `false` returns every match in one response (up to 5000). */
|
|
453
|
+
paginate?: boolean;
|
|
408
454
|
signal?: AbortSignal | null;
|
|
409
455
|
} = {}): Promise<Page<Row>> {
|
|
410
456
|
let query = this.query();
|
|
@@ -417,6 +463,7 @@ export class ResourceClient<
|
|
|
417
463
|
if (params.expand) query = query.expand(...params.expand);
|
|
418
464
|
if (params.page) query = query.page(params.page);
|
|
419
465
|
if (params.perPage) query = query.perPage(params.perPage);
|
|
466
|
+
if (params.paginate === false) query = query.paginate(false);
|
|
420
467
|
if (params.signal) query = query.withOptions({ signal: params.signal });
|
|
421
468
|
return query.execute();
|
|
422
469
|
}
|
package/src/errors.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pawabase answers errors in several shapes: a bare string (most `4xx`), a bare
|
|
5
5
|
* array of validation issues (`422`), an `{error, message, details}` object
|
|
6
|
-
* (
|
|
6
|
+
* (flows, the gateway) and Akountz's own `{detail}` bodies. The
|
|
7
7
|
* client normalises all of them into one class hierarchy, so application code
|
|
8
8
|
* checks `error.status`, `error.code` or `instanceof` and never parses a body.
|
|
9
9
|
*/
|
|
@@ -277,6 +277,11 @@ export function errorFromResponse(input: {
|
|
|
277
277
|
else if (typeof body["code"] === "string") code = body["code"] as string;
|
|
278
278
|
if (typeof body["message"] === "string") message = body["message"] as string;
|
|
279
279
|
else if (typeof detail === "string") message = detail;
|
|
280
|
+
// OpenAI-style bodies, which the AI gateway answers with: { error: { message, type, code } }.
|
|
281
|
+
if (isRecord(named)) {
|
|
282
|
+
if (!message && typeof named["message"] === "string") message = named["message"] as string;
|
|
283
|
+
if (typeof named["code"] === "string") code = named["code"] as string;
|
|
284
|
+
}
|
|
280
285
|
if (Array.isArray(detail)) {
|
|
281
286
|
issues = toIssues(detail);
|
|
282
287
|
message ||= summarise(issues);
|