@myapihq/sdk 1.3.5 → 1.3.8
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/dist/funnel.d.ts +20 -0
- package/dist/funnel.js +23 -0
- package/dist/llm.d.ts +84 -12
- package/dist/llm.js +20 -0
- package/dist/workflow.js +28 -0
- package/package.json +1 -1
- package/src/funnel.ts +49 -0
- package/src/llm.ts +120 -17
- package/src/workflow.ts +26 -0
package/dist/funnel.d.ts
CHANGED
|
@@ -4,9 +4,26 @@ export interface Funnel {
|
|
|
4
4
|
id: string;
|
|
5
5
|
org_id: string;
|
|
6
6
|
name?: string;
|
|
7
|
+
org_webhook_id?: string;
|
|
7
8
|
created_at: string;
|
|
8
9
|
updated_at: string;
|
|
9
10
|
}
|
|
11
|
+
export interface FormFieldSpec {
|
|
12
|
+
name: string;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
oneof?: string[];
|
|
15
|
+
max_length?: number;
|
|
16
|
+
regex?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface FormBinding {
|
|
19
|
+
slug: string;
|
|
20
|
+
destination: string;
|
|
21
|
+
fields?: FormFieldSpec[];
|
|
22
|
+
honeypot_field?: string;
|
|
23
|
+
rate_limit?: {
|
|
24
|
+
per_minute: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
10
27
|
export interface CreateFunnelResponse {
|
|
11
28
|
funnel: Funnel;
|
|
12
29
|
subdomain_url?: string;
|
|
@@ -67,3 +84,6 @@ export interface PublishResult {
|
|
|
67
84
|
published_url: string;
|
|
68
85
|
}
|
|
69
86
|
export declare function publishFiles(apiKey: string, orgId: string, funnelId: string, files: PublishFile[], opts?: PublishOptions): Promise<PublishResult>;
|
|
87
|
+
export declare function createFormBinding(apiKey: string, orgId: string, funnelId: string, binding: FormBinding): Promise<FormBinding>;
|
|
88
|
+
export declare function listFormBindings(apiKey: string, orgId: string, funnelId: string): Promise<FormBinding[]>;
|
|
89
|
+
export declare function deleteFormBinding(apiKey: string, orgId: string, funnelId: string, slug: string): Promise<void>;
|
package/dist/funnel.js
CHANGED
|
@@ -9,6 +9,9 @@ exports.pushFunnelPage = pushFunnelPage;
|
|
|
9
9
|
exports.listFunnelPages = listFunnelPages;
|
|
10
10
|
exports.verifyFunnel = verifyFunnel;
|
|
11
11
|
exports.publishFiles = publishFiles;
|
|
12
|
+
exports.createFormBinding = createFormBinding;
|
|
13
|
+
exports.listFormBindings = listFormBindings;
|
|
14
|
+
exports.deleteFormBinding = deleteFormBinding;
|
|
12
15
|
const client_1 = require("./client");
|
|
13
16
|
const config_1 = require("./config");
|
|
14
17
|
exports.EXPOSES = [
|
|
@@ -20,6 +23,12 @@ exports.EXPOSES = [
|
|
|
20
23
|
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
|
|
21
24
|
'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
|
|
22
25
|
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/files',
|
|
26
|
+
// Backend (2026-06-03): canonical funnel-form proxy lands per-slug
|
|
27
|
+
// bindings — destination (webhook/workflow), fields, honeypot, rate
|
|
28
|
+
// limit. See docs/cross-repo-prompts/cli-funnel-form-canonical-followup-2026-06-04.md.
|
|
29
|
+
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
|
|
30
|
+
'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
|
|
31
|
+
'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}/forms/{slug}',
|
|
23
32
|
];
|
|
24
33
|
async function createFunnel(apiKey, orgId, opts) {
|
|
25
34
|
// v2: accept optional name for N-funnels-per-org. Backend rejects names
|
|
@@ -80,3 +89,17 @@ async function publishFiles(apiKey, orgId, funnelId, files, opts = {}) {
|
|
|
80
89
|
}
|
|
81
90
|
return result.data;
|
|
82
91
|
}
|
|
92
|
+
// ── Form bindings (canonical funnel-form proxy) ────────────────────────────
|
|
93
|
+
function formsBase(orgId, funnelId) {
|
|
94
|
+
return `${config_1.FUNNEL_BASE}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/forms`;
|
|
95
|
+
}
|
|
96
|
+
async function createFormBinding(apiKey, orgId, funnelId, binding) {
|
|
97
|
+
return (0, client_1.request)('POST', formsBase(orgId, funnelId), apiKey, binding);
|
|
98
|
+
}
|
|
99
|
+
async function listFormBindings(apiKey, orgId, funnelId) {
|
|
100
|
+
const res = await (0, client_1.request)('GET', formsBase(orgId, funnelId), apiKey);
|
|
101
|
+
return Array.isArray(res) ? res : (res?.forms ?? []);
|
|
102
|
+
}
|
|
103
|
+
async function deleteFormBinding(apiKey, orgId, funnelId, slug) {
|
|
104
|
+
return (0, client_1.request)('DELETE', `${formsBase(orgId, funnelId)}/${encodeURIComponent(slug)}`, apiKey);
|
|
105
|
+
}
|
package/dist/llm.d.ts
CHANGED
|
@@ -12,15 +12,16 @@ export interface CompleteRequest {
|
|
|
12
12
|
temperature?: number;
|
|
13
13
|
stop?: string[];
|
|
14
14
|
}
|
|
15
|
+
export interface RawUsage {
|
|
16
|
+
input_tokens: number;
|
|
17
|
+
output_tokens?: number;
|
|
18
|
+
cost_cents: number;
|
|
19
|
+
}
|
|
15
20
|
export interface CompleteResponse {
|
|
16
21
|
model: string;
|
|
17
22
|
content: string;
|
|
18
23
|
finish_reason: string;
|
|
19
|
-
usage:
|
|
20
|
-
input_tokens: number;
|
|
21
|
-
output_tokens: number;
|
|
22
|
-
cost_usd: number;
|
|
23
|
-
};
|
|
24
|
+
usage: RawUsage;
|
|
24
25
|
}
|
|
25
26
|
export interface EmbedRequest {
|
|
26
27
|
model: string;
|
|
@@ -29,18 +30,15 @@ export interface EmbedRequest {
|
|
|
29
30
|
export interface EmbedResponse {
|
|
30
31
|
model: string;
|
|
31
32
|
embeddings: number[][];
|
|
32
|
-
usage:
|
|
33
|
-
input_tokens: number;
|
|
34
|
-
cost_usd: number;
|
|
35
|
-
};
|
|
33
|
+
usage: RawUsage;
|
|
36
34
|
}
|
|
37
35
|
export interface Model {
|
|
38
36
|
id: string;
|
|
39
37
|
kind: 'chat' | 'embed';
|
|
40
|
-
|
|
38
|
+
context_window?: number;
|
|
41
39
|
dimensions?: number;
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
input_cost_per_1m_cents: number;
|
|
41
|
+
output_cost_per_1m_cents?: number;
|
|
44
42
|
}
|
|
45
43
|
export interface ModelsResponse {
|
|
46
44
|
models: Model[];
|
|
@@ -48,3 +46,77 @@ export interface ModelsResponse {
|
|
|
48
46
|
export declare function complete(apiKey: string, orgId: string, req: CompleteRequest): Promise<CompleteResponse>;
|
|
49
47
|
export declare function embed(apiKey: string, orgId: string, req: EmbedRequest): Promise<EmbedResponse>;
|
|
50
48
|
export declare function listModels(apiKey: string, orgId: string): Promise<ModelsResponse>;
|
|
49
|
+
export type Verb = 'classify' | 'extract' | 'summarize' | 'draft';
|
|
50
|
+
/** Routing hint; opaque — the server picks the model. With one served
|
|
51
|
+
* model today every tier resolves to it. `tier_used` echoes back what was
|
|
52
|
+
* actually selected. */
|
|
53
|
+
export type Tier = 'fast' | 'reasoning' | 'cheap';
|
|
54
|
+
export interface VerbUsage {
|
|
55
|
+
tier_used: Tier;
|
|
56
|
+
tokens_in: number;
|
|
57
|
+
tokens_out: number;
|
|
58
|
+
cost_cents: number;
|
|
59
|
+
}
|
|
60
|
+
interface VerbBase {
|
|
61
|
+
/** Routing hint. Defaults to 'fast'. */
|
|
62
|
+
tier?: Tier;
|
|
63
|
+
}
|
|
64
|
+
export interface ClassifyRequest extends VerbBase {
|
|
65
|
+
input: string;
|
|
66
|
+
/** Candidate labels — pick from this set. */
|
|
67
|
+
labels: string[];
|
|
68
|
+
/** When true, multiple labels may apply; response carries `labels[]`. */
|
|
69
|
+
multi?: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface ClassifyResponse {
|
|
72
|
+
data: {
|
|
73
|
+
label?: string;
|
|
74
|
+
labels?: string[];
|
|
75
|
+
};
|
|
76
|
+
usage: VerbUsage;
|
|
77
|
+
}
|
|
78
|
+
export interface ExtractRequest extends VerbBase {
|
|
79
|
+
input: string;
|
|
80
|
+
/** A JSON Schema (object) the extracted data must conform to. */
|
|
81
|
+
schema: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
export interface ExtractResponse {
|
|
84
|
+
data: {
|
|
85
|
+
data: Record<string, unknown>;
|
|
86
|
+
};
|
|
87
|
+
usage: VerbUsage;
|
|
88
|
+
}
|
|
89
|
+
export interface SummarizeRequest extends VerbBase {
|
|
90
|
+
input: string;
|
|
91
|
+
/** `brief` (1-2 sentences), `exec` (3-4 decision-maker sentences), or
|
|
92
|
+
* `bullet` (short bulleted list). Default `brief`. */
|
|
93
|
+
style?: 'brief' | 'exec' | 'bullet';
|
|
94
|
+
}
|
|
95
|
+
export interface SummarizeResponse {
|
|
96
|
+
data: {
|
|
97
|
+
summary: string;
|
|
98
|
+
};
|
|
99
|
+
usage: VerbUsage;
|
|
100
|
+
}
|
|
101
|
+
export interface DraftRequest extends VerbBase {
|
|
102
|
+
/** Optional source content — may be empty when `context`/`prompt` carry
|
|
103
|
+
* the brief. */
|
|
104
|
+
input?: string;
|
|
105
|
+
/** What to write — `email`, `message`, `reply`, etc. */
|
|
106
|
+
kind: string;
|
|
107
|
+
/** Free-form structured context (recipient, tone, facts). */
|
|
108
|
+
context?: Record<string, unknown>;
|
|
109
|
+
/** Free-text instructions to the writer. */
|
|
110
|
+
prompt?: string;
|
|
111
|
+
}
|
|
112
|
+
export interface DraftResponse {
|
|
113
|
+
data: {
|
|
114
|
+
text: string;
|
|
115
|
+
};
|
|
116
|
+
usage: VerbUsage;
|
|
117
|
+
}
|
|
118
|
+
export declare function classify(apiKey: string, orgId: string, req: ClassifyRequest): Promise<ClassifyResponse>;
|
|
119
|
+
export declare function extract(apiKey: string, orgId: string, req: ExtractRequest): Promise<ExtractResponse>;
|
|
120
|
+
export declare function summarize(apiKey: string, orgId: string, req: SummarizeRequest): Promise<SummarizeResponse>;
|
|
121
|
+
export declare function draft(apiKey: string, orgId: string, req: DraftRequest): Promise<DraftResponse>;
|
|
122
|
+
export {};
|
package/dist/llm.js
CHANGED
|
@@ -9,12 +9,17 @@ exports.EXPOSES = void 0;
|
|
|
9
9
|
exports.complete = complete;
|
|
10
10
|
exports.embed = embed;
|
|
11
11
|
exports.listModels = listModels;
|
|
12
|
+
exports.classify = classify;
|
|
13
|
+
exports.extract = extract;
|
|
14
|
+
exports.summarize = summarize;
|
|
15
|
+
exports.draft = draft;
|
|
12
16
|
const client_1 = require("./client");
|
|
13
17
|
const config_1 = require("./config");
|
|
14
18
|
exports.EXPOSES = [
|
|
15
19
|
'POST /llm/orgs/{org_id}/complete',
|
|
16
20
|
'POST /llm/orgs/{org_id}/embed',
|
|
17
21
|
'GET /llm/orgs/{org_id}/models',
|
|
22
|
+
'POST /llm/orgs/{org_id}/tasks/{verb}',
|
|
18
23
|
];
|
|
19
24
|
async function complete(apiKey, orgId, req) {
|
|
20
25
|
return (0, client_1.request)('POST', `${config_1.LLM_BASE}/llm/orgs/${encodeURIComponent(orgId)}/complete`, apiKey, req);
|
|
@@ -25,3 +30,18 @@ async function embed(apiKey, orgId, req) {
|
|
|
25
30
|
async function listModels(apiKey, orgId) {
|
|
26
31
|
return (0, client_1.request)('GET', `${config_1.LLM_BASE}/llm/orgs/${encodeURIComponent(orgId)}/models`, apiKey);
|
|
27
32
|
}
|
|
33
|
+
function verbUrl(orgId, verb) {
|
|
34
|
+
return `${config_1.LLM_BASE}/llm/orgs/${encodeURIComponent(orgId)}/tasks/${verb}`;
|
|
35
|
+
}
|
|
36
|
+
async function classify(apiKey, orgId, req) {
|
|
37
|
+
return (0, client_1.request)('POST', verbUrl(orgId, 'classify'), apiKey, req);
|
|
38
|
+
}
|
|
39
|
+
async function extract(apiKey, orgId, req) {
|
|
40
|
+
return (0, client_1.request)('POST', verbUrl(orgId, 'extract'), apiKey, req);
|
|
41
|
+
}
|
|
42
|
+
async function summarize(apiKey, orgId, req) {
|
|
43
|
+
return (0, client_1.request)('POST', verbUrl(orgId, 'summarize'), apiKey, req);
|
|
44
|
+
}
|
|
45
|
+
async function draft(apiKey, orgId, req) {
|
|
46
|
+
return (0, client_1.request)('POST', verbUrl(orgId, 'draft'), apiKey, req);
|
|
47
|
+
}
|
package/dist/workflow.js
CHANGED
|
@@ -23,7 +23,33 @@ exports.EXPOSES = [
|
|
|
23
23
|
'GET /workflow/orgs/{org_id}/workflows/{workflow_id}/runs',
|
|
24
24
|
'GET /workflow/orgs/{org_id}/runs/{run_id}',
|
|
25
25
|
];
|
|
26
|
+
// Local URL-shape check on http_request steps. Same shape as the SSRF
|
|
27
|
+
// audit's scheme allowlist on webhook.forward_url and queue.consumer_url —
|
|
28
|
+
// this is *UX defense*, not security: the backend MUST also validate
|
|
29
|
+
// (which it does, via the same middleware) — see
|
|
30
|
+
// docs/cross-repo-prompts/backend-workflow-step-validation.md.
|
|
31
|
+
// Catches javascript:, file:, gopher:, data:, and whitespace/null-byte
|
|
32
|
+
// hosts at agent typo-fix latency instead of at "why didn't my workflow
|
|
33
|
+
// fire" investigation latency.
|
|
34
|
+
function validateSteps(steps) {
|
|
35
|
+
for (let i = 0; i < steps.length; i++) {
|
|
36
|
+
const s = steps[i];
|
|
37
|
+
if (s.type === 'http_request' || s.type === 'http') {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(s.url);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`step ${i} (${s.type}): "url" is not a valid URL — got ${JSON.stringify(s.url)}.`);
|
|
44
|
+
}
|
|
45
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
46
|
+
throw new Error(`step ${i} (${s.type}): "url" must use http:// or https:// scheme — got "${parsed.protocol}" in ${JSON.stringify(s.url)}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
26
51
|
async function createWorkflow(apiKey, orgId, payload) {
|
|
52
|
+
validateSteps(payload.steps);
|
|
27
53
|
return (0, client_1.request)('POST', `${config_1.WORKFLOW_BASE}/workflow/orgs/${encodeURIComponent(orgId)}/workflows`, apiKey, payload);
|
|
28
54
|
}
|
|
29
55
|
async function listWorkflows(apiKey, orgId) {
|
|
@@ -33,6 +59,8 @@ async function getWorkflow(apiKey, orgId, workflowId) {
|
|
|
33
59
|
return (0, client_1.request)('GET', `${config_1.WORKFLOW_BASE}/workflow/orgs/${encodeURIComponent(orgId)}/workflows/${encodeURIComponent(workflowId)}`, apiKey);
|
|
34
60
|
}
|
|
35
61
|
async function updateWorkflow(apiKey, orgId, workflowId, payload) {
|
|
62
|
+
if (payload.steps)
|
|
63
|
+
validateSteps(payload.steps);
|
|
36
64
|
return (0, client_1.request)('PATCH', `${config_1.WORKFLOW_BASE}/workflow/orgs/${encodeURIComponent(orgId)}/workflows/${encodeURIComponent(workflowId)}`, apiKey, payload);
|
|
37
65
|
}
|
|
38
66
|
async function enableWorkflow(apiKey, orgId, workflowId) {
|
package/package.json
CHANGED
package/src/funnel.ts
CHANGED
|
@@ -12,6 +12,12 @@ export const EXPOSES: Exposes = [
|
|
|
12
12
|
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
|
|
13
13
|
'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
|
|
14
14
|
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/files',
|
|
15
|
+
// Backend (2026-06-03): canonical funnel-form proxy lands per-slug
|
|
16
|
+
// bindings — destination (webhook/workflow), fields, honeypot, rate
|
|
17
|
+
// limit. See docs/cross-repo-prompts/cli-funnel-form-canonical-followup-2026-06-04.md.
|
|
18
|
+
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
|
|
19
|
+
'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/forms',
|
|
20
|
+
'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}/forms/{slug}',
|
|
15
21
|
];
|
|
16
22
|
|
|
17
23
|
export interface Funnel {
|
|
@@ -21,9 +27,33 @@ export interface Funnel {
|
|
|
21
27
|
// to the org's preview_subdomain for back-compat. Get / list responses
|
|
22
28
|
// include it when set.
|
|
23
29
|
name?: string;
|
|
30
|
+
// Backend (2026-06-03): every funnel auto-provisions a webhook_endpoints
|
|
31
|
+
// row at create. Submissions to `/submit/{slug}` without a per-slug
|
|
32
|
+
// binding fall through to this webhook. Optional only because legacy
|
|
33
|
+
// funnels migrated by the boot job may be briefly null.
|
|
34
|
+
org_webhook_id?: string;
|
|
24
35
|
created_at: string;
|
|
25
36
|
updated_at: string;
|
|
26
37
|
}
|
|
38
|
+
|
|
39
|
+
// Per-slug binding from `POST /funnel/.../forms`. Backend resolves a
|
|
40
|
+
// submission's destination at submit time: per-slug binding wins, then
|
|
41
|
+
// `Funnel.org_webhook_id`, else `NO_DESTINATION`.
|
|
42
|
+
export interface FormFieldSpec {
|
|
43
|
+
name: string;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
oneof?: string[];
|
|
46
|
+
max_length?: number;
|
|
47
|
+
regex?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface FormBinding {
|
|
50
|
+
slug: string;
|
|
51
|
+
// "<kind>:<uuid>" — kind is "webhook" or "workflow".
|
|
52
|
+
destination: string;
|
|
53
|
+
fields?: FormFieldSpec[];
|
|
54
|
+
honeypot_field?: string;
|
|
55
|
+
rate_limit?: { per_minute: number };
|
|
56
|
+
}
|
|
27
57
|
export interface CreateFunnelResponse { funnel: Funnel; subdomain_url?: string; domain_url?: string }
|
|
28
58
|
export interface VerifyResult {
|
|
29
59
|
pages?: Array<{ slug: string; tests: VerifyTest[] }>;
|
|
@@ -138,3 +168,22 @@ export async function publishFiles(
|
|
|
138
168
|
}
|
|
139
169
|
return (result as ApiResponse<PublishResult>).data as PublishResult;
|
|
140
170
|
}
|
|
171
|
+
|
|
172
|
+
// ── Form bindings (canonical funnel-form proxy) ────────────────────────────
|
|
173
|
+
|
|
174
|
+
function formsBase(orgId: string, funnelId: string): string {
|
|
175
|
+
return `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/forms`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function createFormBinding(apiKey: string, orgId: string, funnelId: string, binding: FormBinding): Promise<FormBinding> {
|
|
179
|
+
return request('POST', formsBase(orgId, funnelId), apiKey, binding);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function listFormBindings(apiKey: string, orgId: string, funnelId: string): Promise<FormBinding[]> {
|
|
183
|
+
const res = await request<FormBinding[] | { forms?: FormBinding[] }>('GET', formsBase(orgId, funnelId), apiKey);
|
|
184
|
+
return Array.isArray(res) ? res : (res?.forms ?? []);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function deleteFormBinding(apiKey: string, orgId: string, funnelId: string, slug: string): Promise<void> {
|
|
188
|
+
return request('DELETE', `${formsBase(orgId, funnelId)}/${encodeURIComponent(slug)}`, apiKey);
|
|
189
|
+
}
|
package/src/llm.ts
CHANGED
|
@@ -12,8 +12,15 @@ export const EXPOSES: Exposes = [
|
|
|
12
12
|
'POST /llm/orgs/{org_id}/complete',
|
|
13
13
|
'POST /llm/orgs/{org_id}/embed',
|
|
14
14
|
'GET /llm/orgs/{org_id}/models',
|
|
15
|
+
'POST /llm/orgs/{org_id}/tasks/{verb}',
|
|
15
16
|
];
|
|
16
17
|
|
|
18
|
+
// ── Raw surface ──────────────────────────────────────────────────────────
|
|
19
|
+
// Backed *exclusively* by self-hosted open-source models (Qwen on our TPU).
|
|
20
|
+
// The customer picks the model id from the catalog. Proprietary models are
|
|
21
|
+
// not callable here — that rule is what keeps the LLM slot off the
|
|
22
|
+
// reseller framing.
|
|
23
|
+
|
|
17
24
|
export type Role = 'system' | 'user' | 'assistant';
|
|
18
25
|
|
|
19
26
|
export interface Message {
|
|
@@ -29,15 +36,17 @@ export interface CompleteRequest {
|
|
|
29
36
|
stop?: string[];
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
export interface RawUsage {
|
|
40
|
+
input_tokens: number;
|
|
41
|
+
output_tokens?: number;
|
|
42
|
+
cost_cents: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export interface CompleteResponse {
|
|
33
46
|
model: string;
|
|
34
47
|
content: string;
|
|
35
|
-
finish_reason: string; // 'stop' | 'length' | '
|
|
36
|
-
usage:
|
|
37
|
-
input_tokens: number;
|
|
38
|
-
output_tokens: number;
|
|
39
|
-
cost_usd: number;
|
|
40
|
-
};
|
|
48
|
+
finish_reason: string; // 'stop' | 'length' | 'filter'
|
|
49
|
+
usage: RawUsage;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export interface EmbedRequest {
|
|
@@ -47,26 +56,22 @@ export interface EmbedRequest {
|
|
|
47
56
|
|
|
48
57
|
// Always returned as an array — even for a single-string input you get a
|
|
49
58
|
// one-element array. Each element is the dense vector as a flat number[].
|
|
50
|
-
// Embed-only usage has no output_tokens.
|
|
51
59
|
export interface EmbedResponse {
|
|
52
60
|
model: string;
|
|
53
61
|
embeddings: number[][];
|
|
54
|
-
usage:
|
|
55
|
-
input_tokens: number;
|
|
56
|
-
cost_usd: number;
|
|
57
|
-
};
|
|
62
|
+
usage: RawUsage;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
// Catalog row.
|
|
61
|
-
// have dimensions instead. Pricing is per 1M tokens
|
|
62
|
-
// (
|
|
65
|
+
// Catalog row. `chat` models have context_window + output_cost_per_1m_cents;
|
|
66
|
+
// `embed` models have dimensions instead. Pricing is in cents per 1M tokens
|
|
67
|
+
// (the platform's native unit — cost_cents everywhere).
|
|
63
68
|
export interface Model {
|
|
64
69
|
id: string;
|
|
65
70
|
kind: 'chat' | 'embed';
|
|
66
|
-
|
|
71
|
+
context_window?: number;
|
|
67
72
|
dimensions?: number;
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
input_cost_per_1m_cents: number;
|
|
74
|
+
output_cost_per_1m_cents?: number;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface ModelsResponse {
|
|
@@ -84,3 +89,101 @@ export async function embed(apiKey: string, orgId: string, req: EmbedRequest): P
|
|
|
84
89
|
export async function listModels(apiKey: string, orgId: string): Promise<ModelsResponse> {
|
|
85
90
|
return request('GET', `${BASE_URL}/llm/orgs/${encodeURIComponent(orgId)}/models`, apiKey);
|
|
86
91
|
}
|
|
92
|
+
|
|
93
|
+
// ── Verb surface — objective-based completion ────────────────────────────
|
|
94
|
+
// The customer asks for a task done (classify / extract / summarize /
|
|
95
|
+
// draft); the model is implementation detail and is never named in the
|
|
96
|
+
// response. This is where any future proprietary model lives — wrapped, not
|
|
97
|
+
// resold.
|
|
98
|
+
|
|
99
|
+
export type Verb = 'classify' | 'extract' | 'summarize' | 'draft';
|
|
100
|
+
|
|
101
|
+
/** Routing hint; opaque — the server picks the model. With one served
|
|
102
|
+
* model today every tier resolves to it. `tier_used` echoes back what was
|
|
103
|
+
* actually selected. */
|
|
104
|
+
export type Tier = 'fast' | 'reasoning' | 'cheap';
|
|
105
|
+
|
|
106
|
+
export interface VerbUsage {
|
|
107
|
+
tier_used: Tier;
|
|
108
|
+
tokens_in: number;
|
|
109
|
+
tokens_out: number;
|
|
110
|
+
cost_cents: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface VerbBase {
|
|
114
|
+
/** Routing hint. Defaults to 'fast'. */
|
|
115
|
+
tier?: Tier;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ClassifyRequest extends VerbBase {
|
|
119
|
+
input: string;
|
|
120
|
+
/** Candidate labels — pick from this set. */
|
|
121
|
+
labels: string[];
|
|
122
|
+
/** When true, multiple labels may apply; response carries `labels[]`. */
|
|
123
|
+
multi?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ClassifyResponse {
|
|
127
|
+
data: { label?: string; labels?: string[] };
|
|
128
|
+
usage: VerbUsage;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ExtractRequest extends VerbBase {
|
|
132
|
+
input: string;
|
|
133
|
+
/** A JSON Schema (object) the extracted data must conform to. */
|
|
134
|
+
schema: Record<string, unknown>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ExtractResponse {
|
|
138
|
+
data: { data: Record<string, unknown> };
|
|
139
|
+
usage: VerbUsage;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SummarizeRequest extends VerbBase {
|
|
143
|
+
input: string;
|
|
144
|
+
/** `brief` (1-2 sentences), `exec` (3-4 decision-maker sentences), or
|
|
145
|
+
* `bullet` (short bulleted list). Default `brief`. */
|
|
146
|
+
style?: 'brief' | 'exec' | 'bullet';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface SummarizeResponse {
|
|
150
|
+
data: { summary: string };
|
|
151
|
+
usage: VerbUsage;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface DraftRequest extends VerbBase {
|
|
155
|
+
/** Optional source content — may be empty when `context`/`prompt` carry
|
|
156
|
+
* the brief. */
|
|
157
|
+
input?: string;
|
|
158
|
+
/** What to write — `email`, `message`, `reply`, etc. */
|
|
159
|
+
kind: string;
|
|
160
|
+
/** Free-form structured context (recipient, tone, facts). */
|
|
161
|
+
context?: Record<string, unknown>;
|
|
162
|
+
/** Free-text instructions to the writer. */
|
|
163
|
+
prompt?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface DraftResponse {
|
|
167
|
+
data: { text: string };
|
|
168
|
+
usage: VerbUsage;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function verbUrl(orgId: string, verb: Verb): string {
|
|
172
|
+
return `${BASE_URL}/llm/orgs/${encodeURIComponent(orgId)}/tasks/${verb}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function classify(apiKey: string, orgId: string, req: ClassifyRequest): Promise<ClassifyResponse> {
|
|
176
|
+
return request('POST', verbUrl(orgId, 'classify'), apiKey, req);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function extract(apiKey: string, orgId: string, req: ExtractRequest): Promise<ExtractResponse> {
|
|
180
|
+
return request('POST', verbUrl(orgId, 'extract'), apiKey, req);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function summarize(apiKey: string, orgId: string, req: SummarizeRequest): Promise<SummarizeResponse> {
|
|
184
|
+
return request('POST', verbUrl(orgId, 'summarize'), apiKey, req);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function draft(apiKey: string, orgId: string, req: DraftRequest): Promise<DraftResponse> {
|
|
188
|
+
return request('POST', verbUrl(orgId, 'draft'), apiKey, req);
|
|
189
|
+
}
|
package/src/workflow.ts
CHANGED
|
@@ -64,11 +64,36 @@ export interface WorkflowRun {
|
|
|
64
64
|
created_at: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Local URL-shape check on http_request steps. Same shape as the SSRF
|
|
68
|
+
// audit's scheme allowlist on webhook.forward_url and queue.consumer_url —
|
|
69
|
+
// this is *UX defense*, not security: the backend MUST also validate
|
|
70
|
+
// (which it does, via the same middleware) — see
|
|
71
|
+
// docs/cross-repo-prompts/backend-workflow-step-validation.md.
|
|
72
|
+
// Catches javascript:, file:, gopher:, data:, and whitespace/null-byte
|
|
73
|
+
// hosts at agent typo-fix latency instead of at "why didn't my workflow
|
|
74
|
+
// fire" investigation latency.
|
|
75
|
+
function validateSteps(steps: WorkflowStep[]): void {
|
|
76
|
+
for (let i = 0; i < steps.length; i++) {
|
|
77
|
+
const s = steps[i];
|
|
78
|
+
if (s.type === 'http_request' || s.type === 'http') {
|
|
79
|
+
let parsed: URL;
|
|
80
|
+
try { parsed = new URL(s.url); }
|
|
81
|
+
catch {
|
|
82
|
+
throw new Error(`step ${i} (${s.type}): "url" is not a valid URL — got ${JSON.stringify(s.url)}.`);
|
|
83
|
+
}
|
|
84
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
85
|
+
throw new Error(`step ${i} (${s.type}): "url" must use http:// or https:// scheme — got "${parsed.protocol}" in ${JSON.stringify(s.url)}.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
export async function createWorkflow(apiKey: string, orgId: string, payload: {
|
|
68
92
|
name: string;
|
|
69
93
|
trigger_config: { endpoint_id: string };
|
|
70
94
|
steps: WorkflowStep[];
|
|
71
95
|
}): Promise<Workflow> {
|
|
96
|
+
validateSteps(payload.steps);
|
|
72
97
|
return request('POST', `${BASE_URL}/workflow/orgs/${encodeURIComponent(orgId)}/workflows`, apiKey, payload);
|
|
73
98
|
}
|
|
74
99
|
|
|
@@ -81,6 +106,7 @@ export async function getWorkflow(apiKey: string, orgId: string, workflowId: str
|
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
export async function updateWorkflow(apiKey: string, orgId: string, workflowId: string, payload: { name?: string; trigger_config?: { endpoint_id: string }; steps?: WorkflowStep[] }): Promise<Workflow> {
|
|
109
|
+
if (payload.steps) validateSteps(payload.steps);
|
|
84
110
|
return request('PATCH', `${BASE_URL}/workflow/orgs/${encodeURIComponent(orgId)}/workflows/${encodeURIComponent(workflowId)}`, apiKey, payload);
|
|
85
111
|
}
|
|
86
112
|
|