@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 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 a=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??B.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},B=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),G={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function K(n){let e=G[n];return e||(n>=500?"server_error":n>=400?"invalid_request":"unknown")}async function q(n){let e=n.status,r="";try{r=await n.text()}catch{}let t=H(r);return new a({code:t?.code??K(e),type:t?.type,message:t?.message??z(e,n.statusText,r),status:e,retryAfterMs:Q(n.headers.get("retry-after")),runId:n.headers.get("soba-run-id")??void 0})}function H(n){if(!n)return null;let e;try{e=JSON.parse(n)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let t=r;return{message:typeof t.message=="string"&&t.message?t.message:void 0,type:typeof t.type=="string"&&t.type?t.type:void 0,code:J.has(t.code)?t.code:void 0}}var J=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 z(n,e,r){let t=r.trim().slice(0,200),o=t?`: ${t}`:"";return`Soba returned ${n}${e?` ${e}`:""}${o}`}function Q(n){if(!n)return;let e=Number(n);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(n);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function w(n){if(n instanceof a)return n;let e=n;return e?.name==="AbortError"||e?.name==="TimeoutError"?new a({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:n}):new a({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:n})}var E="0.0.11";var Y=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function V(n){if(n.code==="rate_limited")return!0;if(n.code!=="connection")return!1;let e=n.cause;for(let r=0;e&&r<4;r++){let t=e.code;if(typeof t=="string"&&Y.has(t))return!0;e=e.cause}return!1}var X=8e3,Z=3e4;async function b(n,e){let r=ne(n.baseURL,e.path,e.query),t=e.retry===!1?1:Math.max(0,n.maxRetries)+1,o,s=i=>i.retryable&&(e.retry!=="unsent"||V(i));for(let i=0;i<t;i++){i>0&&await oe(re(i,o),e.signal);let u=new AbortController,d=A(e.signal,u),h=setTimeout(()=>{u.abort(new a({code:"timeout",message:`Soba did not answer within ${n.connectTimeoutMs}ms.`}))},n.connectTimeoutMs),l;try{l=await n.fetch(r,{method:e.method,headers:ee(n,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(d(),o=w(u.signal.aborted?u.signal.reason??p:p),!s(o)||i===t-1)throw o;continue}finally{clearTimeout(h)}if(l.ok)return l;if(d(),o=await q(l),!s(o)||i===t-1)throw o}throw o??new a({code:"unknown",message:"The request failed with no error."})}async function R(n,e){let r=await b(n,{accept:"application/json",...e}),t=await r.text();if(t)try{return JSON.parse(t)}catch(o){throw new a({code:"server_error",message:`Soba returned a body that is not JSON: ${t.slice(0,200)}`,status:r.status,cause:o})}}function ee(n,e){let r={authorization:`Bearer ${n.apiKey}`,accept:e.accept??"application/json","user-agent":te(),...n.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function te(){let n=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${E}${n}`}function ne(n,e,r){let t=n.replace(/\/+$/,""),o=new URL(`${t}${e.startsWith("/")?e:`/${e}`}`);for(let[s,i]of Object.entries(r??{}))i!==void 0&&o.searchParams.set(s,String(i));return o.toString()}function re(n,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Z);let r=Math.min(X,500*2**(n-1));return Math.round(r*(.5+Math.random()/2))}function oe(n,e){return new Promise((r,t)=>{if(e?.aborted)return t(w(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},n);function s(){clearTimeout(o),t(w(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function A(n,e){if(!n)return()=>{};if(n.aborted)return e.abort(n.reason),()=>{};let r=()=>e.abort(n.reason);return n.addEventListener("abort",r,{once:!0}),()=>n.removeEventListener("abort",r)}var M=["open-weights","frontier"];function se(n){return M.includes(n)}function ie(n){return n==="user-hardware"||n==="user-subscription"||n==="user-api-key"||n==="open-weights"||n==="frontier"}function _(n){return n.type==="done"||n.type==="error"}var ae=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var N=new Set(["delta","thinking","status","done","error"]);function C(n){let e;try{e=JSON.parse(n)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,t=c(r.runId);if(typeof r.type=="string"&&N.has(r.type)){let o=O(r);return o?{kind:"event",runId:t,event:o}:null}switch(r.type){case"event":{if(!r.event||typeof r.event!="object")return null;let o=O(r.event);return o?{kind:"event",runId:t,event:o}:null}case"usage":{let o=$(r.usage);return o?{kind:"usage",runId:t,usage:o}:null}case"tool_call":{let o=c(r.callId),s=c(r.name);return!o||!s?null:{kind:"tool_call",runId:t,callId:o,name:s,input:r.input,...r.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=c(r.approvalId),s=c(r.tool);return!o||!s?null:{kind:"approval",runId:t,approvalId:o,tool:s,input:r.input,cwd:c(r.cwd),...r.answered===!0?{answered:!0}:{}}}default:return null}}function O(n){let e=typeof n.type=="string"&&N.has(n.type)?n.type:null;if(!e)return null;let r={type:e},t=c(n.text);t!==void 0&&(r.text=t);let o=c(n.message);o!==void 0&&(r.message=o);let s=c(n.tool);s!==void 0&&(r.tool=s);let i=c(n.tier);i!==void 0&&(r.tier=i);let u=$(n.usage);u&&(r.usage=u);let d=c(n.reason);d!==void 0&&(r.reason=d),typeof n.retryable=="boolean"&&(r.retryable=n.retryable);let h=n.turn&&typeof n.turn=="object"?c(n.turn.text):void 0;h!==void 0&&(r.turn={text:h});let l=c(n.session);l!==void 0&&(r.session=l);let p=c(n.toolInput);p!==void 0&&(r.toolInput=p);let m=ue(n.git);return m&&(r.git=m),(n.state==="interrupted"||n.state==="resumed")&&(r.state=n.state),r}function ue(n){if(!n||typeof n!="object")return null;let e=n,r=c(e.branch);return!r||!Array.isArray(e.files)||!Array.isArray(e.commits)?null:{branch:r,base:c(e.base)??"",head:c(e.head)??"",ahead:y(e.ahead),behind:y(e.behind),files:e.files.filter(t=>!!t&&typeof t=="object"&&typeof t.path=="string").map(t=>({path:t.path,status:t.status==="A"||t.status==="D"||t.status==="R"?t.status:"M",added:y(t.added),removed:y(t.removed)})),commits:e.commits.filter(t=>!!t&&typeof t=="object").map(t=>({sha:c(t.sha)??"",subject:c(t.subject)??""})),dirty:e.dirty===!0}}function $(n){if(!n||typeof n!="object")return null;let e=n,r={model:c(e.model)??"unknown",inputTokens:y(e.inputTokens),outputTokens:y(e.outputTokens),cacheReadTokens:y(e.cacheReadTokens),cacheWriteTokens:y(e.cacheWriteTokens),webSearches:y(e.webSearches)},t=c(e.costClass);t!==void 0&&(r.costClass=t);let o=c(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function c(n){return typeof n=="string"&&n.length>0?n:void 0}function y(n){return typeof n=="number"&&Number.isFinite(n)?n:0}async function*F(n){let e=n.body;if(!e)throw new a({code:"connection",message:"The run response arrived with no body to stream.",status:n.status});let r=n.headers.get("content-type")??"",t=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,i="",u=[],d,h=l=>d===void 0?l:{...l,seq:d};try{for(;;){let p=await o.read();if(p.done)break;for(i+=s.decode(p.value,{stream:!0});;){let m=i.indexOf(`
2
- `);if(m===-1)break;let f=L(i.slice(0,m));if(i=i.slice(m+1),t){let g=f.trim()?C(f):null;g&&(yield g);continue}if(f===""){let g=u.join(`
3
- `);if(u=[],g==="[DONE]")return;let U=g?C(g):null;U&&(yield h(U)),d=void 0;continue}if(f.startsWith("id:")){let g=Number(f.slice(3).trim());d=Number.isInteger(g)&&g>0?g:void 0;continue}D(f,u)}}!t&&i&&D(L(i),u);let l=t?i.trim():u.join(`
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};
@@ -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 {
@@ -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.11";
1
+ export declare const VERSION = "0.0.13";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soba-so/sdk",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "One request that comes back finished, executed on compute the end user owns.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://soba.so/docs/runs",