@soba-so/sdk 0.0.11 → 0.0.13
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/client.d.ts +18 -0
- package/dist/errors.d.ts +1 -6
- package/dist/fallback.d.ts +143 -0
- package/dist/http.d.ts +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +7 -4
- package/dist/protocol.d.ts +8 -0
- package/dist/resources.d.ts +6 -0
- package/dist/run.d.ts +16 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -40,6 +40,23 @@ export interface SobaOptions {
|
|
|
40
40
|
* visitor the ability to spend your account. Only set this for a trusted
|
|
41
41
|
* first-party context you control. */
|
|
42
42
|
dangerouslyAllowBrowser?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Answer runs with your own provider when Soba cannot be reached. On by
|
|
45
|
+
* default, and it does nothing until "Also use if Soba can't be reached" is
|
|
46
|
+
* switched on under Models in your dashboard. `false` turns it off here
|
|
47
|
+
* whatever the dashboard says. See `soba.fallback`.
|
|
48
|
+
*/
|
|
49
|
+
fallback?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Your own provider answering while Soba cannot be reached. See `fallback.ts`. */
|
|
52
|
+
export interface FallbackControl {
|
|
53
|
+
/** Read what to fall back to now rather than on the first run. Never throws. */
|
|
54
|
+
prime(): Promise<void>;
|
|
55
|
+
/** Report the runs served while Soba was unreachable. Call it from `waitUntil`
|
|
56
|
+
* on serverless; it is also tried after every run Soba answers. */
|
|
57
|
+
flush(): Promise<void>;
|
|
58
|
+
/** Runs served without Soba and not yet reported. */
|
|
59
|
+
readonly pending: number;
|
|
43
60
|
}
|
|
44
61
|
export interface RunParams {
|
|
45
62
|
/**
|
|
@@ -250,6 +267,7 @@ export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal"
|
|
|
250
267
|
*/
|
|
251
268
|
export declare class Soba {
|
|
252
269
|
#private;
|
|
270
|
+
readonly fallback: FallbackControl;
|
|
253
271
|
readonly users: {
|
|
254
272
|
/** What this user has: plan, allowance, usage this period, what they connected. */
|
|
255
273
|
get(id: string): Promise<SobaUser>;
|
package/dist/errors.d.ts
CHANGED
|
@@ -61,12 +61,7 @@ export declare class SobaError extends Error {
|
|
|
61
61
|
constructor(init: SobaErrorInit);
|
|
62
62
|
}
|
|
63
63
|
export declare function codeForStatus(status: number): SobaErrorCode;
|
|
64
|
-
|
|
65
|
-
* Build an error from a failed response. The envelope is
|
|
66
|
-
* `{ error: { type, message, code } }` on every endpoint, but a proxy, a
|
|
67
|
-
* gateway timeout page or an HTML error can arrive here too, so nothing about
|
|
68
|
-
* the body is assumed.
|
|
69
|
-
*/
|
|
64
|
+
export declare function fromSobaEdge(err: SobaError): boolean;
|
|
70
65
|
export declare function errorFromResponse(response: Response): Promise<SobaError>;
|
|
71
66
|
/** RFC 7231 allows seconds or an HTTP date. Both appear in the wild. */
|
|
72
67
|
export declare function retryAfterMs(header: string | null): number | undefined;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type HttpConfig } from "./http.js";
|
|
2
|
+
import type { Message, RouteRequest, ToolSchema } from "./protocol.js";
|
|
3
|
+
/**
|
|
4
|
+
* WHEN SOBA ITSELF CANNOT BE REACHED, YOUR OWN PROVIDER ANSWERS.
|
|
5
|
+
*
|
|
6
|
+
* Your providers (the dashboard's Models page) already catch a run your users'
|
|
7
|
+
* own AI cannot serve — but Soba's gateway does the catching, so with the
|
|
8
|
+
* gateway down your server got a refused connection and your users got
|
|
9
|
+
* nothing, on a provider account you pay for and could have called yourself.
|
|
10
|
+
*
|
|
11
|
+
* With "Also use if Soba can't be reached" switched on, this calls that
|
|
12
|
+
* provider directly from your server instead: the same OpenAI-compatible
|
|
13
|
+
* endpoint the gateway would have called, your tools answered here, and the
|
|
14
|
+
* same events back from `soba.run()`. Each run it serves is reported to Soba
|
|
15
|
+
* once Soba answers again (`flush`), so usage and allowances are late rather
|
|
16
|
+
* than wrong.
|
|
17
|
+
*
|
|
18
|
+
* WHERE THE PROVIDER COMES FROM, in order:
|
|
19
|
+
*
|
|
20
|
+
* 1. What Soba last said (`GET /v1/fallback`), read while it was up and kept
|
|
21
|
+
* in memory — the key included, if Soba holds it. Nothing to set.
|
|
22
|
+
* 2. `SOBA_FALLBACK`, the line the dashboard gives you to paste, for a server
|
|
23
|
+
* that starts while Soba is already down and so never read (1) — the
|
|
24
|
+
* usual case on serverless. It holds no key: the key comes from the
|
|
25
|
+
* variable the provider names (`SOBA_SUPPLIER_…_KEY`), which you set.
|
|
26
|
+
*
|
|
27
|
+
* WHEN IT ENGAGES: only when the run provably never reached Soba — a refused
|
|
28
|
+
* connection, a name that did not resolve, or an error page from in front of
|
|
29
|
+
* the gateway rather than from it. Not on a timeout, and never once Soba has
|
|
30
|
+
* answered: either can follow a run Soba already started, and answering it
|
|
31
|
+
* again here would be a second run, paid for twice.
|
|
32
|
+
*/
|
|
33
|
+
export interface FallbackProvider {
|
|
34
|
+
/** The provider's id on the Models page, which a reported run is priced by. */
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
baseUrl: string;
|
|
38
|
+
/** The variable read for the key, ahead of `apiKey`. */
|
|
39
|
+
keyEnv: string;
|
|
40
|
+
costClass: string;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
models: {
|
|
43
|
+
model: string;
|
|
44
|
+
alias: string | null;
|
|
45
|
+
maxOutputTokens: number | null;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
interface FallbackConfig {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
providers: FallbackProvider[];
|
|
51
|
+
}
|
|
52
|
+
/** A run this served, as it is reported to Soba (`POST /v1/usage/backfill`). */
|
|
53
|
+
export interface FallbackRecord {
|
|
54
|
+
id: string;
|
|
55
|
+
user: string;
|
|
56
|
+
supplier: string;
|
|
57
|
+
model: string;
|
|
58
|
+
input_tokens: number;
|
|
59
|
+
output_tokens: number;
|
|
60
|
+
cache_read_tokens: number;
|
|
61
|
+
cache_write_tokens: number;
|
|
62
|
+
started_at: string;
|
|
63
|
+
ended_at: string;
|
|
64
|
+
status: "done" | "error";
|
|
65
|
+
error?: string;
|
|
66
|
+
task?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface FallbackRequest {
|
|
69
|
+
runId: string;
|
|
70
|
+
user: string;
|
|
71
|
+
model: string | undefined;
|
|
72
|
+
route: RouteRequest | undefined;
|
|
73
|
+
system: string | undefined;
|
|
74
|
+
messages: Message[];
|
|
75
|
+
tools: ToolSchema[];
|
|
76
|
+
task: string | undefined;
|
|
77
|
+
}
|
|
78
|
+
/** A run's tool result, handed back by `Run` instead of posted to Soba. */
|
|
79
|
+
export interface ToolAnswer {
|
|
80
|
+
callId: string;
|
|
81
|
+
result: unknown;
|
|
82
|
+
isError: boolean;
|
|
83
|
+
}
|
|
84
|
+
export declare class Fallback {
|
|
85
|
+
#private;
|
|
86
|
+
constructor(init: {
|
|
87
|
+
http: HttpConfig;
|
|
88
|
+
onWarning: (message: string, detail?: unknown) => void;
|
|
89
|
+
off: boolean;
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Read what to fall back to now, rather than on the first run. For a server
|
|
93
|
+
* that starts while Soba is up and wants to be ready before it goes down.
|
|
94
|
+
* Never throws: with Soba unreachable there is simply nothing new to keep.
|
|
95
|
+
*/
|
|
96
|
+
prime(): Promise<void>;
|
|
97
|
+
/** Called on every run: a read in the background when what is kept is old. */
|
|
98
|
+
refreshSoon(): void;
|
|
99
|
+
/** Runs served here and not yet reported to Soba. */
|
|
100
|
+
get pending(): number;
|
|
101
|
+
/**
|
|
102
|
+
* Whether this error means the run never reached Soba, and something here
|
|
103
|
+
* can answer it instead.
|
|
104
|
+
*/
|
|
105
|
+
canServe(err: unknown, route: RouteRequest | undefined): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* The run, answered by the vendor's own provider: Soba's stream format, so
|
|
108
|
+
* `Run` reads it exactly as it reads one from the gateway.
|
|
109
|
+
*/
|
|
110
|
+
serve(request: FallbackRequest, parent: AbortSignal | undefined): {
|
|
111
|
+
response: Response;
|
|
112
|
+
deliver: (answer: ToolAnswer) => void;
|
|
113
|
+
stop: (reason: unknown) => void;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Report the runs served here, now. Safe to call any time and from several
|
|
117
|
+
* places at once; a run Soba already has is dropped, not counted twice.
|
|
118
|
+
* On serverless, call it where the platform lets work finish after the
|
|
119
|
+
* response (`waitUntil`) — a function frozen between requests reports
|
|
120
|
+
* nothing until it next runs.
|
|
121
|
+
*/
|
|
122
|
+
flush(): Promise<void>;
|
|
123
|
+
/** After a run Soba answered: it is up, so whatever is queued can go. */
|
|
124
|
+
flushSoon(): void;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `SOBA_FALLBACK`: the dashboard's providers as base64url JSON, keys left out.
|
|
128
|
+
* Pasted into the deploy's environment, so a server that never reached Soba
|
|
129
|
+
* still knows where to go. Its presence is the vendor opting in; Soba saying
|
|
130
|
+
* otherwise (`enabled: false`, read later) still wins.
|
|
131
|
+
*/
|
|
132
|
+
export declare function fromEnv(): FallbackConfig | null;
|
|
133
|
+
/**
|
|
134
|
+
* Which of this provider's models answers.
|
|
135
|
+
*
|
|
136
|
+
* A model it lists by name or alias, as the picker sends it. Otherwise what
|
|
137
|
+
* the gateway would pick: the cheapest (models arrive cheapest first) for
|
|
138
|
+
* Automatic and Fast, the dearest for Best — and a name only a machine knows,
|
|
139
|
+
* like `opus` for Claude Code, is Automatic here, because the run has to go
|
|
140
|
+
* somewhere.
|
|
141
|
+
*/
|
|
142
|
+
export declare function pickModel(provider: FallbackProvider, wanted: string | undefined): FallbackProvider["models"][number];
|
|
143
|
+
export {};
|
package/dist/http.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface RequestOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
retry?: boolean | "unsent";
|
|
30
30
|
}
|
|
31
|
+
export declare function provablyUnsent(err: SobaError): boolean;
|
|
31
32
|
export declare function httpRequest(cfg: HttpConfig, opts: RequestOptions): Promise<Response>;
|
|
32
33
|
export declare function requestJson<T>(cfg: HttpConfig, opts: RequestOptions): Promise<T>;
|
|
33
34
|
export declare function buildUrl(baseURL: string, path: string, query?: Record<string, string | number | boolean | undefined>): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { type AttachParams, DEFAULT_BASE_URL, DEFAULT_WEB_BASE_URL, type DetachedRunParams, type OpKind, type OpParams, type OpResult, type RunParams, Soba, type SobaOptions, type StartedRun, type TerminalBooking, type TerminalParams, } from "./client.js";
|
|
1
|
+
export { type AttachParams, DEFAULT_BASE_URL, DEFAULT_WEB_BASE_URL, type DetachedRunParams, type FallbackControl, type OpKind, type OpParams, type OpResult, type RunParams, Soba, type SobaOptions, type StartedRun, type TerminalBooking, type TerminalParams, } from "./client.js";
|
|
2
2
|
export { SobaError, type SobaErrorCode } from "./errors.js";
|
|
3
|
-
export { type AgentEvent, type AgentEventType, type CostClass, type GitSnapshot, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
|
|
3
|
+
export { type AgentEvent, type AgentEventType, type CostClass, type GitSnapshot, type ImageInput, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
|
|
4
4
|
export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, FeatureCheckResult, FeatureDenial, MachineListItem, MachineListState, MachineRuntime, PlanFeatureWire, PlanListItem, PlanListResponse, PlanSummary, QuantityResult, RuntimeListItem, SobaUser, TrackResult, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
|
|
5
5
|
export { type ApprovalDecision, type ApprovalRequest, type Approver, Run, } from "./run.js";
|
|
6
6
|
export { type ToolContext, type ToolDefinition, tool } from "./tools.js";
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
var
|
|
2
|
-
|
|
3
|
-
`);
|
|
4
|
-
`);if(l&&l!=="[DONE]"){let p=C(l);p&&(yield t?p:h(p))}}finally{o.cancel().catch(()=>{})}}function D(n,e){if(n.startsWith(":"))return;let r=n.indexOf(":");if(r===-1||n.slice(0,r)!=="data")return;let t=n.slice(r+1);e.push(t.startsWith(" ")?t.slice(1):t)}function L(n){return n.endsWith("\r")?n.slice(0,-1):n}function ce(n){return n}function S(n){let e=new Map;for(let r of n??[]){if(!r?.name)throw new a({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new a({code:"invalid_request",message:`Tool "${r.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(r.name))throw new a({code:"invalid_request",message:`Two tools are named "${r.name}". Tool names must be unique within a run.`});e.set(r.name,r)}return e}function I(n){return[...n.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function v(n){if(n===void 0)return null;if(typeof n=="string")return n;try{return JSON.parse(JSON.stringify(n))}catch{return String(n)}}function T(n){return n instanceof Error?n.message||n.name:typeof n=="string"?n:String(n)}var k=class{#e;#n;#r;#u=!1;#o;#t;#s=new AbortController;#i=new AbortController;#a=new Set;#c;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#o=e.after??0,this.#c=A(e.signal,this.#s)}get cursor(){return this.#o}get resumable(){return this.#t.response.headers.get("soba-resumable")==="1"}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#l()}async text(){let e="";for await(let r of this)if(r.type==="delta"&&r.text&&(e+=r.text),r.type==="error")throw new a({code:"run_failed",message:r.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#s.abort(new a({code:"canceled",message:"The run was cancelled."})),this.#i.abort(new a({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await b(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#l(){if(this.#u)throw new a({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#u=!0;let e=!1;try{for await(let r of F(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.seq!==void 0&&(this.#o=r.seq),r.kind==="event"){let t=r.event;if(t.tier&&(this.#n=t.tier),t.usage&&(this.#r=t.usage),e=_(t),yield t,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#p(this.#g(r))}throw new a({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#i.abort(new a({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#a]),this.#s.abort(),this.#c()}}#p(e){this.#a.add(e),e.finally(()=>this.#a.delete(e)).catch(()=>{})}async#g(e){if(!((e.kind==="tool_call"||e.kind==="approval")&&e.answered)){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#m(e)}}async#f(e){let r=this.#t.tools.get(e.name),t,o=!1;if(!r)t=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#i.signal};try{t=await r.execute(e.input,s)}catch(i){t=T(i),o=!0}}await this.#d(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:v(t),isError:o})}async#m(e){let r="deny",t;if(!this.#t.approve)t="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),r=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){t=T(o)}await this.#d(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...t?{reason:t}:{}})}async#d(e,r){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",r);return}try{await(await b(this.#t.config,{method:"POST",path:e,body:r})).text()}catch(t){this.#t.onWarning(`Could not deliver an answer to ${e}.`,w(t))}}};var j="https://api.soba.so/v1",W="https://soba.so/v1",de=3e4,le=2,P=class{#e;#n;#r;users;usage;connect;plans;runs;ops;terminals;constructor(e={}){let r=e.apiKey??x("SOBA_KEY")??x("SOBA_API_KEY");if(!r)throw new a({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(pe()&&!e.dangerouslyAllowBrowser)throw new a({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#r=e.onWarning??(()=>{}),this.#e={apiKey:r,baseURL:e.baseURL??x("SOBA_BASE_URL")??j,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??de,maxRetries:e.maxRetries??le},this.#n={...this.#e,baseURL:e.webBaseURL??x("SOBA_WEB_URL")??W},typeof this.#e.fetch!="function")throw new a({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:t=>this.request("GET",`/users/${encodeURIComponent(t)}`),compute:async t=>(await this.request("GET",`/users/${encodeURIComponent(t)}/compute`)).machines,setQuantity:(t,o,s)=>R(this.#n,{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/quantity`,body:{quantity:s}}),check:(t,o,s)=>R(s?.use?{...this.#n,maxRetries:0}:this.#n,{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/check`,body:s?.use?{use:s.use}:{}}),track:(t,o,s=1)=>R({...this.#n,maxRetries:0},{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/track`,body:{amount:s}})},this.usage={list:t=>this.request("GET","/usage",{query:{...t}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let o=S(t.tools),s={user:t.user,model:t.model??"auto",detached:!0};t.webhook&&(s.webhook=t.webhook),t.prompt&&(s.prompt=t.prompt),t.messages?.length&&(s.messages=t.messages),t.system&&(s.system=t.system),t.runtime&&(s.runtime=t.runtime),o.size&&(s.tools=I(o)),t.route&&(s.route=t.route),t.policy&&(s.policy=t.policy),t.metadata&&(s.metadata=t.metadata),t.task&&(s.task=t.task),t.surface&&(s.surface=t.surface),t.maxDurationMs&&(s.maxDurationMs=t.maxDurationMs),t.title&&(s.title=t.title),t.worktree&&(s.worktree=t.worktree),t.resume&&(s.resume=t.resume);let i=await R(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"}),u={id:i.id};return i.stream&&(u.stream={url:i.stream.url,token:i.stream.token,expiresAt:i.stream.expires_at}),t.answerTools==="here"&&(u.answered=(async()=>{let d=await this.runs.attach(i.id,{user:t.user,tools:t.tools??[]});for await(let h of d);})().catch(d=>this.#r("Answering this run's tool calls stopped.",d))),u},attach:async(t,o)=>{if(!o.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let s=await b(this.#e,{method:"GET",path:`/runs/${encodeURIComponent(t)}/stream`,...o.after?{query:{after:o.after}}:{},accept:"text/event-stream",signal:o.signal});return new k({config:this.#e,response:s,user:o.user,tools:S(o.tools),approve:o.approve,onWarning:this.#r,signal:o.signal,after:o.after??0})},cancel:async t=>{await this.request("POST",`/runs/${encodeURIComponent(t)}/cancel`,{body:{}})}},this.ops={run:t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to ask."});return this.request("POST","/ops",{body:{...t}})}},this.terminals={open:async t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to open it on."});let o=await this.request("POST","/term",{body:{...t}}),s=new URL(this.#e.baseURL);s.protocol=s.protocol==="http:"?"ws:":"wss:";let i=new URL(o.path,`${s.protocol}//${s.host}`);return i.searchParams.set("ticket",o.ticket),{termId:o.termId,url:i.toString(),expiresAt:o.expiresAt}}},this.plans={list:t=>this.request("GET","/plans",{...t?.user?{query:{user:t.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new a({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new a({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let r=S(e.tools),t={user:e.user,model:e.model??"auto"};e.prompt&&(t.prompt=e.prompt),e.messages?.length&&(t.messages=e.messages),e.system&&(t.system=e.system),e.runtime&&(t.runtime=e.runtime),r.size&&(t.tools=I(r)),e.route&&(t.route=e.route),e.policy&&(t.policy=e.policy),e.metadata&&(t.metadata=e.metadata),e.task&&(t.task=e.task),e.resumable&&(t.resumable=!0),e.maxDurationMs&&(t.maxDurationMs=e.maxDurationMs);let o=await b(this.#e,{method:"POST",path:"/runs",body:t,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new k({config:this.#e,response:o,user:e.user,tools:r,approve:e.approve,onWarning:this.#r,signal:e.signal})}async request(e,r,t={}){return R(this.#e,{method:e,path:r,...t})}};function x(n){if(typeof process>"u")return;let e=process.env?.[n];return e||void 0}function pe(){let n=globalThis;return typeof n.window=="object"&&typeof n.window?.document=="object"}var ge=300;function fe(n){return[...new Uint8Array(n)].map(e=>e.toString(16).padStart(2,"0")).join("")}function me(n,e){if(n.length!==e.length)return!1;let r=0;for(let t=0;t<n.length;t++)r|=n.charCodeAt(t)^e.charCodeAt(t);return r===0}async function he(n,e,r,t={}){let o=Object.fromEntries((e??"").split(",").map(f=>{let g=f.indexOf("=");return[f.slice(0,g).trim(),f.slice(g+1).trim()]})),s=Number(o.t),i=o.v1??"",u=f=>new a({code:"unauthorized",status:401,message:f});if(!Number.isFinite(s)||!i)throw u("Missing or malformed Soba-Signature.");let d=Math.floor((t.now??Date.now())/1e3);if(Math.abs(d-s)>(t.toleranceSeconds??ge))throw u("This delivery is too old to accept.");let h=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=fe(await globalThis.crypto.subtle.sign("HMAC",h,new TextEncoder().encode(`${s}.${n}`)));if(!me(l,i))throw u("The Soba-Signature does not match.");let p;try{p=JSON.parse(n)}catch{throw new a({code:"invalid_request",message:"The delivery is not JSON."})}let m=p;if(m.type!=="run.event"&&m.type!=="run.tool_call"||typeof m.run_id!="string")throw new a({code:"invalid_request",message:"Not a Soba run delivery."});return m}async function ye(n,e){if(n.type!=="run.tool_call")return null;let{call_id:r,name:t,input:o}=n.data,s=S(e.tools).get(t);if(!s)return{result:`Unknown tool: ${t}`,is_error:!0};try{let i=await s.execute(o??{},{runId:n.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:v(i)}}catch(i){return{result:T(i),is_error:!0}}}export{j as DEFAULT_BASE_URL,W as DEFAULT_WEB_BASE_URL,M as METERED_CLASSES,k as Run,P as Soba,a as SobaError,ae as TOOL_SLUGS,E as VERSION,ye as handleRunWebhook,ie as isCostClass,se as isMetered,_ as isTerminal,ce as tool,he as verifyWebhook};
|
|
1
|
+
var u=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??oe.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},oe=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),se={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function ie(t){let e=se[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}var W=new WeakSet;function j(t){return W.has(t)}async function B(t){let e=t.status,n="";try{n=await t.text()}catch{}let r=ae(n),o=new u({code:r?.code??ie(e),type:r?.type,message:r?.message??le(e,t.statusText,n),status:e,retryAfterMs:ce(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0});return!r&&(e===502||e===503||e===504)&&W.add(o),o}function ae(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let n=e.error;if(!n||typeof n!="object")return null;let r=n;return{message:typeof r.message=="string"&&r.message?r.message:void 0,type:typeof r.type=="string"&&r.type?r.type:void 0,code:ue.has(r.code)?r.code:void 0}}var ue=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","unknown_run","not_resumable","machine_gone","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function le(t,e,n){let r=n.trim().slice(0,200),o=r?`: ${r}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function ce(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let n=Date.parse(t);if(!Number.isNaN(n))return Math.max(0,n-Date.now())}function w(t){if(t instanceof u)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new u({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new u({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var q="0.0.13";var de=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function F(t){if(t.code==="rate_limited")return!0;if(t.code!=="connection")return!1;let e=t.cause;for(let n=0;e&&n<4;n++){let r=e.code;if(typeof r=="string"&&de.has(r))return!0;e=e.cause}return!1}var pe=8e3,fe=3e4;async function R(t,e){let n=he(t.baseURL,e.path,e.query),r=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o,s=i=>i.retryable&&(e.retry!=="unsent"||F(i));for(let i=0;i<r;i++){i>0&&await D(be(i,o),e.signal);let a=new AbortController,c=A(e.signal,a),m=setTimeout(()=>{a.abort(new u({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),p;try{p=await t.fetch(n,{method:e.method,headers:ge(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:a.signal})}catch(g){if(c(),o=w(a.signal.aborted?a.signal.reason??g:g),!s(o)||i===r-1)throw o;continue}finally{clearTimeout(m)}if(p.ok)return p;if(c(),o=await B(p),!s(o)||i===r-1)throw o}throw o??new u({code:"unknown",message:"The request failed with no error."})}async function S(t,e){let n=await R(t,{accept:"application/json",...e}),r=await n.text();if(r)try{return JSON.parse(r)}catch(o){throw new u({code:"server_error",message:`Soba returned a body that is not JSON: ${r.slice(0,200)}`,status:n.status,cause:o})}}function ge(t,e){let n={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":me(),...t.headers};return e.body!==void 0&&(n["content-type"]="application/json"),n}function me(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${q}${t}`}function he(t,e,n){let r=t.replace(/\/+$/,""),o=new URL(`${r}${e.startsWith("/")?e:`/${e}`}`);for(let[s,i]of Object.entries(n??{}))i!==void 0&&o.searchParams.set(s,String(i));return o.toString()}function be(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,fe);let n=Math.min(pe,500*2**(t-1));return Math.round(n*(.5+Math.random()/2))}function D(t,e){return new Promise((n,r)=>{if(e?.aborted)return r(w(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),n()},t);function s(){clearTimeout(o),r(w(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function A(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let n=()=>e.abort(t.reason);return t.addEventListener("abort",n,{once:!0}),()=>t.removeEventListener("abort",n)}var ye=5*60*1e3,G=25,we=1e3,ke=100,P=class{#e;#o;#r;#s=null;#i=0;#t=null;#n=[];#a=null;constructor(e){this.#e=e.http,this.#o=e.onWarning,this.#r=e.off}async prime(){if(!this.#r)return this.#t??=S(this.#e,{method:"GET",path:"/fallback",retry:!1}).then(e=>{this.#s={enabled:e?.enabled===!0,providers:(e?.providers??[]).map(K).filter(J)},this.#i=Date.now()}).catch(()=>{}).finally(()=>{this.#t=null}),this.#t}refreshSoon(){this.#r||Date.now()-this.#i<ye||this.prime()}get pending(){return this.#n.length}canServe(e,n){if(this.#r)return!1;let r=w(e);return!F(r)&&!j(r)||r.code==="rate_limited"?!1:this.#c(n).length>0}serve(e,n){let r=new AbortController,o=A(n,r),s=r.signal,i=new Map,a=new Map,c=l=>{let d=i.get(l.callId);d?(i.delete(l.callId),d(l)):a.set(l.callId,l)},m=l=>new Promise((d,y)=>{let b=a.get(l);if(b)return a.delete(l),d(b);i.set(l,d),s.addEventListener("abort",()=>y(w(s.reason)),{once:!0})}),p=this.#c(e.route),g=new TextEncoder,f=new ReadableStream({start:l=>{let d=y=>{try{l.enqueue(g.encode(`data: ${JSON.stringify(y)}
|
|
2
|
+
|
|
3
|
+
`))}catch{}};this.#d(e,p,d,m,s).catch(y=>d({type:"error",message:w(y).message})).finally(()=>{o();try{l.close()}catch{}})},cancel:l=>r.abort(l)});return{response:new Response(f,{status:200,headers:{"content-type":"text/event-stream","soba-run-id":e.runId}}),deliver:c,stop:l=>r.abort(l)}}async flush(){return this.#a??=this.#u().finally(()=>{this.#a=null}),this.#a}flushSoon(){this.#n.length&&this.flush()}async#u(){for(;this.#n.length;){let e=this.#n.slice(0,ke),n;try{n=(await S(this.#e,{method:"POST",path:"/usage/backfill",body:{runs:e}}))?.results??[]}catch{return}let r=new Set(n.filter(s=>s.outcome==="failed").map(s=>s.id)),o=new Set(e.map(s=>s.id));for(let s of n)s.outcome==="rejected"&&this.#o("Soba would not record a run served while it was unreachable.",s);if(this.#n=[...e.filter(s=>r.has(s.id)),...this.#n.filter(s=>!o.has(s.id))],r.size===e.length)return}}#l(e){this.#n.push(e),this.#n.length>we&&(this.#n.shift(),this.#o("Dropped the oldest unreported fallback run: too many waiting for Soba."))}#c(e){let n=this.#s??ve();return n?.enabled?n.providers.filter(r=>H(r)&&(!e?.allow||e.allow.includes(r.costClass))):[]}async#d(e,n,r,o,s){let i=new Date,a="None of your providers could be reached.";for(let c of n){let m=Se(c,e.model),p={inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheWriteTokens:0},g=!1,f=(l,d)=>this.#l({id:e.runId,user:e.user,supplier:c.id,model:m.model,input_tokens:p.inputTokens,output_tokens:p.outputTokens,cache_read_tokens:p.cacheReadTokens,cache_write_tokens:p.cacheWriteTokens,started_at:i.toISOString(),ended_at:new Date().toISOString(),status:l,...d?{error:d}:{},...e.task?{task:e.task}:{}});try{r({type:"status",tier:c.costClass,message:`Soba can't be reached, so ${c.name} is answering.`});let l=Te(e);for(let y=0;;y++){if(y>=G)throw new Error(`Stopped after ${G} turns of tool calls.`);let b=await Re(this.#e.fetch,c,m,l,e.tools,s,k=>{g=!0,r({type:"delta",text:k})});if(p.inputTokens+=b.inputTokens,p.outputTokens+=b.outputTokens,p.cacheReadTokens+=b.cacheReadTokens,!b.toolCalls.length)break;g=!0,l.push({role:"assistant",content:b.text||null,tool_calls:b.toolCalls});for(let k of b.toolCalls)r({type:"tool_call",callId:k.id,name:k.function.name,input:_e(k.function.arguments)});for(let k of b.toolCalls){let v=await o(k.id);l.push({role:"tool",tool_call_id:k.id,content:typeof v.result=="string"?v.result:JSON.stringify(v.result)})}}let d={model:m.model,provider:c.id,costClass:c.costClass,...p,webSearches:0};r({type:"usage",usage:d}),r({type:"done",usage:d}),f("done");return}catch(l){if(s.aborted)throw f("error","cancelled"),w(s.reason);if(a=`${c.name}: ${w(l).message}`,g)throw f("error",a),new u({code:"run_failed",message:a,retryable:!1})}}throw new u({code:"no_compute",message:a,retryable:!1})}};function K(t){if(!t?.id||!t.base_url||!t.key_env||!t.cost_class)return null;let e=(t.models??[]).filter(n=>typeof n?.model=="string"&&n.model).map(n=>({model:n.model,alias:n.alias??null,maxOutputTokens:n.max_output_tokens??null}));return e.length?{id:t.id,name:t.name??t.id,baseUrl:t.base_url,keyEnv:t.key_env,costClass:t.cost_class,...t.api_key?{apiKey:t.api_key}:{},models:e}:null}var J=t=>t!==null;function ve(){let t=z("SOBA_FALLBACK");if(!t)return null;try{let e=atob(t.replace(/-/g,"+").replace(/_/g,"/")),r=(JSON.parse(e).providers??[]).map(K).filter(J);return r.length?{enabled:!0,providers:r}:null}catch{return null}}function H(t){return z(t.keyEnv)??t.apiKey}function Se(t,e){let n=t.models,r=e?.startsWith(`@${t.id}/`)?e.slice(t.id.length+2):e,o=n.find(s=>s.model===r||s.alias===r);return o||(e==="large"||e==="best"?n[n.length-1]:n[0])}function Te(t){let e=[];t.system&&e.push({role:"system",content:t.system});for(let n of t.messages)n.role==="user"&&n.images?.length?e.push({role:"user",content:[{type:"text",text:n.content},...n.images.map(r=>({type:"image_url",image_url:{url:`data:${r.mediaType};base64,${r.data}`}}))]}):n.role==="user"?e.push({role:"user",content:n.content}):e.push({role:"assistant",content:n.content});return e}async function Re(t,e,n,r,o,s,i){let a=await t(`${e.baseUrl.replace(/\/+$/,"")}/chat/completions`,{method:"POST",headers:{authorization:`Bearer ${H(e)}`,"content-type":"application/json",accept:"text/event-stream"},body:JSON.stringify({model:n.model,messages:r,stream:!0,stream_options:{include_usage:!0},...n.maxOutputTokens?{max_tokens:n.maxOutputTokens}:{},...o.length?{tools:o.map(f=>({type:"function",function:{name:f.name,description:f.description,parameters:f.inputSchema}}))}:{}}),signal:s});if(!a.ok||!a.body){let f=await a.text().catch(()=>"");throw new Error(`${a.status} ${f.slice(0,300)}`.trim())}let c={text:"",toolCalls:[],inputTokens:0,outputTokens:0,cacheReadTokens:0},m=a.body.getReader(),p=new TextDecoder,g="";try{for(;;){let f=await m.read();if(f.done)break;for(g+=p.decode(f.value,{stream:!0});;){let l=g.indexOf(`
|
|
4
|
+
`);if(l===-1)break;let d=g.slice(0,l).trim();if(g=g.slice(l+1),!d.startsWith("data:"))continue;let y=d.slice(5).trim();if(!y||y==="[DONE]")continue;let b;try{b=JSON.parse(y)}catch{continue}b.usage&&(c.inputTokens=b.usage.prompt_tokens??0,c.outputTokens=b.usage.completion_tokens??0,c.cacheReadTokens=b.usage.prompt_tokens_details?.cached_tokens??0,c.inputTokens-=c.cacheReadTokens);let k=b.choices?.[0]?.delta;k?.content&&(c.text+=k.content,i(k.content));for(let v of k?.tool_calls??[]){let U=v.index??c.toolCalls.length;c.toolCalls[U]??={id:v.id??`call_${U}`,type:"function",function:{name:"",arguments:""}};let M=c.toolCalls[U];v.id&&(M.id=v.id),v.function?.name&&(M.function.name+=v.function.name),v.function?.arguments&&(M.function.arguments+=v.function.arguments)}}}}finally{m.cancel().catch(()=>{})}return c.toolCalls=c.toolCalls.filter(f=>f?.function.name),c}function _e(t){if(!t)return{};try{return JSON.parse(t)}catch{return{}}}function z(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}var Q=["open-weights","frontier"];function xe(t){return Q.includes(t)}function Ae(t){return t==="user-hardware"||t==="user-subscription"||t==="user-api-key"||t==="open-weights"||t==="frontier"}function L(t){return t.type==="done"||t.type==="error"}var Ee=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var Z=new Set(["delta","thinking","status","done","error"]);function N(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let n=e,r=h(n.runId);if(typeof n.type=="string"&&Z.has(n.type)){let o=Y(n);return o?{kind:"event",runId:r,event:o}:null}switch(n.type){case"event":{if(!n.event||typeof n.event!="object")return null;let o=Y(n.event);return o?{kind:"event",runId:r,event:o}:null}case"usage":{let o=ee(n.usage);return o?{kind:"usage",runId:r,usage:o}:null}case"tool_call":{let o=h(n.callId),s=h(n.name);return!o||!s?null:{kind:"tool_call",runId:r,callId:o,name:s,input:n.input,...n.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=h(n.approvalId),s=h(n.tool);return!o||!s?null:{kind:"approval",runId:r,approvalId:o,tool:s,input:n.input,cwd:h(n.cwd),...n.answered===!0?{answered:!0}:{}}}default:return null}}function Y(t){let e=typeof t.type=="string"&&Z.has(t.type)?t.type:null;if(!e)return null;let n={type:e},r=h(t.text);r!==void 0&&(n.text=r);let o=h(t.message);o!==void 0&&(n.message=o);let s=h(t.tool);s!==void 0&&(n.tool=s);let i=h(t.tier);i!==void 0&&(n.tier=i);let a=ee(t.usage);a&&(n.usage=a);let c=h(t.reason);c!==void 0&&(n.reason=c),typeof t.retryable=="boolean"&&(n.retryable=t.retryable);let m=t.turn&&typeof t.turn=="object"?h(t.turn.text):void 0;m!==void 0&&(n.turn={text:m});let p=h(t.session);p!==void 0&&(n.session=p);let g=h(t.toolInput);g!==void 0&&(n.toolInput=g);let f=Ce(t.git);return f&&(n.git=f),(t.state==="interrupted"||t.state==="resumed")&&(n.state=t.state),n}function Ce(t){if(!t||typeof t!="object")return null;let e=t,n=h(e.branch);return!n||!Array.isArray(e.files)||!Array.isArray(e.commits)?null:{branch:n,base:h(e.base)??"",head:h(e.head)??"",ahead:T(e.ahead),behind:T(e.behind),files:e.files.filter(r=>!!r&&typeof r=="object"&&typeof r.path=="string").map(r=>({path:r.path,status:r.status==="A"||r.status==="D"||r.status==="R"?r.status:"M",added:T(r.added),removed:T(r.removed)})),commits:e.commits.filter(r=>!!r&&typeof r=="object").map(r=>({sha:h(r.sha)??"",subject:h(r.subject)??""})),dirty:e.dirty===!0}}function ee(t){if(!t||typeof t!="object")return null;let e=t,n={model:h(e.model)??"unknown",inputTokens:T(e.inputTokens),outputTokens:T(e.outputTokens),cacheReadTokens:T(e.cacheReadTokens),cacheWriteTokens:T(e.cacheWriteTokens),webSearches:T(e.webSearches)},r=h(e.costClass);r!==void 0&&(n.costClass=r);let o=h(e.provider);return o!==void 0&&(n.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(n.costMicros=e.costMicros),n}function h(t){return typeof t=="string"&&t.length>0?t:void 0}function T(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*te(t){let e=t.body;if(!e)throw new u({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let n=t.headers.get("content-type")??"",r=n.includes("ndjson")||n.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,i="",a=[],c,m=p=>c===void 0?p:{...p,seq:c};try{for(;;){let g=await o.read();if(g.done)break;for(i+=s.decode(g.value,{stream:!0});;){let f=i.indexOf(`
|
|
5
|
+
`);if(f===-1)break;let l=V(i.slice(0,f));if(i=i.slice(f+1),r){let d=l.trim()?N(l):null;d&&(yield d);continue}if(l===""){let d=a.join(`
|
|
6
|
+
`);if(a=[],d==="[DONE]")return;let y=d?N(d):null;y&&(yield m(y)),c=void 0;continue}if(l.startsWith("id:")){let d=Number(l.slice(3).trim());c=Number.isInteger(d)&&d>0?d:void 0;continue}X(l,a)}}!r&&i&&X(V(i),a);let p=r?i.trim():a.join(`
|
|
7
|
+
`);if(p&&p!=="[DONE]"){let g=N(p);g&&(yield r?g:m(g))}}finally{o.cancel().catch(()=>{})}}function X(t,e){if(t.startsWith(":"))return;let n=t.indexOf(":");if(n===-1||t.slice(0,n)!=="data")return;let r=t.slice(n+1);e.push(r.startsWith(" ")?r.slice(1):r)}function V(t){return t.endsWith("\r")?t.slice(0,-1):t}function Pe(t){return t}function x(t){let e=new Map;for(let n of t??[]){if(!n?.name)throw new u({code:"invalid_request",message:"Every tool needs a name."});if(typeof n.execute!="function")throw new u({code:"invalid_request",message:`Tool "${n.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(n.name))throw new u({code:"invalid_request",message:`Two tools are named "${n.name}". Tool names must be unique within a run.`});e.set(n.name,n)}return e}function I(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function E(t){if(t===void 0)return null;if(typeof t=="string")return t;try{return JSON.parse(JSON.stringify(t))}catch{return String(t)}}function C(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var Ie=9e4,_=class{#e;#o;#r;#s=!1;#i;#t;#n=new AbortController;#a=new AbortController;#u=new Set;#l;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#i=e.after??0,this.#l=A(e.signal,this.#n)}get cursor(){return this.#i}get servedWithoutSoba(){return this.#t.local!==void 0}get resumable(){return this.#t.response.headers.get("soba-resumable")==="1"}get runId(){return this.#e}get tier(){return this.#o}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#d()}async text(){let e="";for await(let n of this)if(n.type==="delta"&&n.text&&(e+=n.text),n.type==="error")throw new u({code:"run_failed",message:n.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#n.abort(new u({code:"canceled",message:"The run was cancelled."})),this.#a.abort(new u({code:"canceled",message:"The run was cancelled."})),this.#t.local)return this.#t.local.cancel(this.#n.signal.reason);if(this.#e)try{await R(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async#c(e){let n=Date.now()+Ie,r=e;for(let o=0;Date.now()<n;o++){if(this.#n.signal.aborted)throw w(this.#n.signal.reason);o>0&&await D(Math.min(1e3*o,5e3),this.#n.signal);try{return await R(this.#t.config,{method:"GET",path:`/runs/${encodeURIComponent(this.#e)}/stream`,query:{after:this.#i},accept:"text/event-stream",signal:this.#n.signal,retry:!1})}catch(s){if(r=w(s),!["server_error","connection","timeout","rate_limited"].includes(r.code))break}}throw new u({code:"stream_truncated",message:`The run stream ended without a \`done\` or \`error\` event, and reading on failed: ${r.message}`,runId:this.#e,cause:r})}async*#d(){if(this.#s)throw new u({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#s=!0;let e=!1;try{let n=this.#t.response;for(;;){for await(let o of te(n)){if(o.runId&&!this.#e&&(this.#e=o.runId),o.seq!==void 0&&(this.#i=o.seq),o.kind==="event"){let s=o.event;if(s.tier&&(this.#o=s.tier),s.usage&&(this.#r=s.usage),e=L(s),yield s,e)return;continue}if(o.kind==="usage"){this.#r=o.usage;continue}this.#f(this.#g(o))}let r=new u({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e});if(!this.resumable||!this.#e||this.#t.local)throw r;n=await this.#c(r)}}finally{e||this.#a.abort(new u({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#u]),this.#n.abort(),this.#l()}}#f(e){this.#u.add(e),e.finally(()=>this.#u.delete(e)).catch(()=>{})}async#g(e){if(!((e.kind==="tool_call"||e.kind==="approval")&&e.answered)){if(e.kind==="tool_call")return this.#m(e);if(e.kind==="approval")return this.#h(e)}}async#m(e){let n=this.#t.tools.get(e.name),r,o=!1;if(!n)r=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#a.signal};try{r=await n.execute(e.input,s)}catch(i){r=C(i),o=!0}}if(this.#t.local){this.#t.local.deliver({callId:e.callId,result:E(r),isError:o});return}await this.#p(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:E(r),isError:o})}async#h(e){let n="deny",r;if(!this.#t.approve)r="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),n=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){r=C(o)}await this.#p(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:n,...r?{reason:r}:{}})}async#p(e,n){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",n);return}try{await(await R(this.#t.config,{method:"POST",path:e,body:n})).text()}catch(r){this.#t.onWarning(`Could not deliver an answer to ${e}.`,w(r))}}};var ne="https://api.soba.so/v1",re="https://soba.so/v1",Oe=3e4,Ue=2,$=class{#e;#o;#r;#s;fallback;users;usage;connect;plans;runs;ops;terminals;constructor(e={}){let n=e.apiKey??O("SOBA_KEY")??O("SOBA_API_KEY");if(!n)throw new u({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(Me()&&!e.dangerouslyAllowBrowser)throw new u({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#r=e.onWarning??(()=>{}),this.#e={apiKey:n,baseURL:e.baseURL??O("SOBA_BASE_URL")??ne,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??Oe,maxRetries:e.maxRetries??Ue},this.#o={...this.#e,baseURL:e.webBaseURL??O("SOBA_WEB_URL")??re},typeof this.#e.fetch!="function")throw new u({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});let r=new P({http:this.#e,onWarning:this.#r,off:e.fallback===!1});this.#s=r,this.fallback={prime:()=>r.prime(),flush:()=>r.flush(),get pending(){return r.pending}},this.users={get:o=>this.request("GET",`/users/${encodeURIComponent(o)}`),compute:async o=>(await this.request("GET",`/users/${encodeURIComponent(o)}/compute`)).machines,setQuantity:(o,s,i)=>S(this.#o,{method:"POST",path:`/end_users/${encodeURIComponent(o)}/features/${encodeURIComponent(s)}/quantity`,body:{quantity:i}}),check:(o,s,i)=>S(i?.use?{...this.#o,maxRetries:0}:this.#o,{method:"POST",path:`/end_users/${encodeURIComponent(o)}/features/${encodeURIComponent(s)}/check`,body:i?.use?{use:i.use}:{}}),track:(o,s,i=1)=>S({...this.#o,maxRetries:0},{method:"POST",path:`/end_users/${encodeURIComponent(o)}/features/${encodeURIComponent(s)}/track`,body:{amount:i}})},this.usage={list:o=>this.request("GET","/usage",{query:{...o}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async o=>{if(!o.user)throw new u({code:"user_required",status:422,message:"`user` is required."});let s=x(o.tools),i={user:o.user,model:o.model??"auto",detached:!0};o.webhook&&(i.webhook=o.webhook),o.prompt&&(i.prompt=o.prompt),o.messages?.length&&(i.messages=o.messages),o.system&&(i.system=o.system),o.runtime&&(i.runtime=o.runtime),s.size&&(i.tools=I(s)),o.route&&(i.route=o.route),o.policy&&(i.policy=o.policy),o.metadata&&(i.metadata=o.metadata),o.task&&(i.task=o.task),o.surface&&(i.surface=o.surface),o.maxDurationMs&&(i.maxDurationMs=o.maxDurationMs),o.title&&(i.title=o.title),o.worktree&&(i.worktree=o.worktree),o.resume&&(i.resume=o.resume);let a=await S(this.#e,{method:"POST",path:"/runs",body:i,retry:"unsent"}),c={id:a.id};return a.stream&&(c.stream={url:a.stream.url,token:a.stream.token,expiresAt:a.stream.expires_at}),o.answerTools==="here"&&(c.answered=(async()=>{let m=await this.runs.attach(a.id,{user:o.user,tools:o.tools??[]});for await(let p of m);})().catch(m=>this.#r("Answering this run's tool calls stopped.",m))),c},attach:async(o,s)=>{if(!s.user)throw new u({code:"user_required",status:422,message:"`user` is required."});let i=await R(this.#e,{method:"GET",path:`/runs/${encodeURIComponent(o)}/stream`,...s.after?{query:{after:s.after}}:{},accept:"text/event-stream",signal:s.signal});return new _({config:this.#e,response:i,user:s.user,tools:x(s.tools),approve:s.approve,onWarning:this.#r,signal:s.signal,after:s.after??0})},cancel:async o=>{await this.request("POST",`/runs/${encodeURIComponent(o)}/cancel`,{body:{}})}},this.ops={run:o=>{if(!o.user)throw new u({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to ask."});return this.request("POST","/ops",{body:{...o}})}},this.terminals={open:async o=>{if(!o.user)throw new u({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to open it on."});let s=await this.request("POST","/term",{body:{...o}}),i=new URL(this.#e.baseURL);i.protocol=i.protocol==="http:"?"ws:":"wss:";let a=new URL(s.path,`${i.protocol}//${i.host}`);return a.searchParams.set("ticket",s.ticket),{termId:s.termId,url:a.toString(),expiresAt:s.expiresAt}}},this.plans={list:o=>this.request("GET","/plans",{...o?.user?{query:{user:o.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new u({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new u({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let n=x(e.tools),r={user:e.user,model:e.model??"auto"};e.prompt&&(r.prompt=e.prompt),e.messages?.length&&(r.messages=e.messages),e.system&&(r.system=e.system),e.runtime&&(r.runtime=e.runtime),n.size&&(r.tools=I(n)),e.route&&(r.route=e.route),e.policy&&(r.policy=e.policy),e.metadata&&(r.metadata=e.metadata),e.task&&(r.task=e.task),r.resumable=e.resumable?!0:"restarts",e.maxDurationMs&&(r.maxDurationMs=e.maxDurationMs);let o;try{o=await R(this.#e,{method:"POST",path:"/runs",body:r,accept:"text/event-stream",signal:e.signal,retry:"unsent"})}catch(s){if(!this.#s.canServe(s,e.route))throw s;let i=this.#s.serve({runId:globalThis.crypto.randomUUID(),user:e.user,model:e.model,route:e.route,system:e.system,messages:e.messages?.length?e.messages:[{role:"user",content:e.prompt??""}],tools:I(n),task:e.task},e.signal);return this.#r("Soba could not be reached; your own provider is answering this run.",s),new _({config:this.#e,response:i.response,user:e.user,tools:n,approve:e.approve,onWarning:this.#r,signal:e.signal,local:{deliver:i.deliver,cancel:i.stop}})}return this.#s.refreshSoon(),this.#s.flushSoon(),new _({config:this.#e,response:o,user:e.user,tools:n,approve:e.approve,onWarning:this.#r,signal:e.signal})}async request(e,n,r={}){return S(this.#e,{method:e,path:n,...r})}};function O(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function Me(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}var qe=300;function Fe(t){return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,"0")).join("")}function De(t,e){if(t.length!==e.length)return!1;let n=0;for(let r=0;r<t.length;r++)n|=t.charCodeAt(r)^e.charCodeAt(r);return n===0}async function Le(t,e,n,r={}){let o=Object.fromEntries((e??"").split(",").map(l=>{let d=l.indexOf("=");return[l.slice(0,d).trim(),l.slice(d+1).trim()]})),s=Number(o.t),i=o.v1??"",a=l=>new u({code:"unauthorized",status:401,message:l});if(!Number.isFinite(s)||!i)throw a("Missing or malformed Soba-Signature.");let c=Math.floor((r.now??Date.now())/1e3);if(Math.abs(c-s)>(r.toleranceSeconds??qe))throw a("This delivery is too old to accept.");let m=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(n),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),p=Fe(await globalThis.crypto.subtle.sign("HMAC",m,new TextEncoder().encode(`${s}.${t}`)));if(!De(p,i))throw a("The Soba-Signature does not match.");let g;try{g=JSON.parse(t)}catch{throw new u({code:"invalid_request",message:"The delivery is not JSON."})}let f=g;if(f.type!=="run.event"&&f.type!=="run.tool_call"||typeof f.run_id!="string")throw new u({code:"invalid_request",message:"Not a Soba run delivery."});return f}async function Ne(t,e){if(t.type!=="run.tool_call")return null;let{call_id:n,name:r,input:o}=t.data,s=x(e.tools).get(r);if(!s)return{result:`Unknown tool: ${r}`,is_error:!0};try{let i=await s.execute(o??{},{runId:t.run_id,user:e.user??"",callId:n,signal:AbortSignal.timeout(25e3)});return{result:E(i)}}catch(i){return{result:C(i),is_error:!0}}}export{ne as DEFAULT_BASE_URL,re as DEFAULT_WEB_BASE_URL,Q as METERED_CLASSES,_ as Run,$ as Soba,u as SobaError,Ee as TOOL_SLUGS,q as VERSION,Ne as handleRunWebhook,Ae as isCostClass,xe as isMetered,L as isTerminal,Pe as tool,Le as verifyWebhook};
|
package/dist/protocol.d.ts
CHANGED
|
@@ -93,6 +93,14 @@ export type ToolSlug = (typeof TOOL_SLUGS)[number];
|
|
|
93
93
|
export interface Message {
|
|
94
94
|
role: "user" | "assistant";
|
|
95
95
|
content: string;
|
|
96
|
+
/** Pictures attached to this turn, in order. A runtime that cannot see
|
|
97
|
+
* images gets `content` alone and says so in a status line. */
|
|
98
|
+
images?: ImageInput[];
|
|
99
|
+
}
|
|
100
|
+
/** One attached picture, inline: base64 without the `data:` prefix. */
|
|
101
|
+
export interface ImageInput {
|
|
102
|
+
mediaType: "image/png" | "image/jpeg" | "image/gif" | "image/webp";
|
|
103
|
+
data: string;
|
|
96
104
|
}
|
|
97
105
|
/** An app-defined tool as it travels on the wire: schema only, no implementation. */
|
|
98
106
|
export interface ToolSchema {
|
package/dist/resources.d.ts
CHANGED
|
@@ -110,6 +110,10 @@ export interface PlanListItem {
|
|
|
110
110
|
/** Runs a month on the user's own AI, counted apart from `included_units`.
|
|
111
111
|
* Null is unlimited. Optional so an older server that omits it parses. */
|
|
112
112
|
own_included_units?: number | null;
|
|
113
|
+
/** What `own_included_units` counts: `unit` unless the plan splits —
|
|
114
|
+
* "200 runs on your own AI, 2,000 credits on ours". Optional for older
|
|
115
|
+
* servers, which mean `unit`. */
|
|
116
|
+
own_unit?: string;
|
|
113
117
|
/** What happens past the allowance: `stop` or `meter`. The difference
|
|
114
118
|
* between a wall and a bill, and not guessable from the price. */
|
|
115
119
|
overage: string;
|
|
@@ -163,6 +167,8 @@ export interface CurrentPlanItem {
|
|
|
163
167
|
units_used: number;
|
|
164
168
|
remaining: number | null;
|
|
165
169
|
own_included_units: number | null;
|
|
170
|
+
/** What the own_* figures count. Optional for older servers: `unit`. */
|
|
171
|
+
own_unit?: string;
|
|
166
172
|
own_units_used: number;
|
|
167
173
|
own_remaining: number | null;
|
|
168
174
|
resets_at: string | null;
|
package/dist/run.d.ts
CHANGED
|
@@ -22,6 +22,19 @@ export interface RunInit {
|
|
|
22
22
|
signal: AbortSignal | undefined;
|
|
23
23
|
/** Attaching to a resumable run: the last frame already read. */
|
|
24
24
|
after?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Served on this server because Soba could not be reached (`fallback.ts`):
|
|
27
|
+
* answers go straight to the loop asking for them, and a cancel stops it,
|
|
28
|
+
* with no request to a Soba that is not there.
|
|
29
|
+
*/
|
|
30
|
+
local?: {
|
|
31
|
+
deliver: (answer: {
|
|
32
|
+
callId: string;
|
|
33
|
+
result: unknown;
|
|
34
|
+
isError: boolean;
|
|
35
|
+
}) => void;
|
|
36
|
+
cancel: (reason: unknown) => void;
|
|
37
|
+
};
|
|
25
38
|
}
|
|
26
39
|
/**
|
|
27
40
|
* A run in flight.
|
|
@@ -44,6 +57,9 @@ export declare class Run implements AsyncIterable<AgentEvent> {
|
|
|
44
57
|
* about to hit its time limit, a deploy, a closed tab.
|
|
45
58
|
*/
|
|
46
59
|
get cursor(): number;
|
|
60
|
+
/** Answered by your own provider because Soba could not be reached. Not
|
|
61
|
+
* resumable, and reported to Soba later (`soba.fallback.flush()`). */
|
|
62
|
+
get servedWithoutSoba(): boolean;
|
|
47
63
|
/** Whether the gateway is keeping this run's frames, so it can be attached to again. */
|
|
48
64
|
get resumable(): boolean;
|
|
49
65
|
/** Empty only until the first frame, if the server omitted the header. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.0.
|
|
1
|
+
export declare const VERSION = "0.0.13";
|
package/package.json
CHANGED