@byokit/accounts 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Expose fresh subscription access to host-side capabilities using the app’s own sign-in.
6
+
7
+ ## 0.11.0 (2026-09-30)
8
+
9
+
10
+
11
+ - Add optional `parallelToolCalls` to `respond` and `Accounts.respond`, passed through to ChatGPT as `parallel_tool_calls`; omitted keeps the provider default.
12
+
13
+ ## 0.10.0 (2026-09-30)
14
+
15
+
16
+
17
+ - Export portable `classifyFailure` and `Failure`, with an injectable clock and `REST_MS` fallbacks; retain `classify` as an alias.
18
+
5
19
  ## 0.9.0 (2026-09-30)
6
20
 
7
21
 
package/README.md CHANGED
@@ -122,7 +122,7 @@ and [`examples/pwa`](../../examples/pwa) (browser sign-in).
122
122
  | `offered`, `provider`, `PROVIDERS` | The catalogue: each provider's billing, terms status, reason and source |
123
123
  | `billingWords`, `say`, `WORDS`, `signInError`, `failure`, `clock`, `callbackPage` | The plain sentences every app shows the same way (`words.json`), a time in words, and the page a browser sees after a sign-in |
124
124
  | `respond`, `ResponseError`, `IncompleteError`, `sseReader`, `limitResponse`, `isFunctionCall` | Ask ChatGPT's answers endpoint with a sign-in, with tools, pictures, thinking effort and an answer shape; the error with the words to show and the kind acted on |
125
- | `classify`, `REST_MS` | An error's kind (limit, overload, plan without this use, lapsed sign-in, network) and default rest times |
125
+ | `classifyFailure`, `classify`, `REST_MS` | An error's kind (limit, overload, plan without this use, lapsed sign-in, network) and default rest times |
126
126
  | `planOf`, `claims` | The ChatGPT plan and email behind a sign-in, from its own token |
127
127
  | `deviceStart`, `devicePoll`, `credentialOf`, `portableEngine`, `PORTABLE` | The device-code flow, the sign-in built from a token answer, and the engine under `portable` |
128
128
  | `isolate`, `INHERITED`, `emptyAuthContext` (`/isolate`) | Scrub inherited Pi settings and provider keys; ambient discovery off |
@@ -260,6 +260,11 @@ already have shown partial words. This covers `response.incomplete` events and `
260
260
  whether fetch streams SSE, buffers it, or returns JSON. The account stays signed in and is not put to rest.
261
261
  Successful return values are unchanged.
262
262
 
263
+ Set `parallelToolCalls: false` to request one tool call at a time; `true` allows parallel calls. Both pass through as
264
+ `parallel_tool_calls`; omitting it sends nothing and keeps the provider default. `respond` supports only the ChatGPT
265
+ subscription route; OpenRouter, Grok and Copilot are credential routes without `respond` support, and there is no
266
+ Anthropic response route.
267
+
263
268
  The whole question passes through: `input` takes the turns so far (messages, with `input_image` where the person
264
269
  attached a picture), `tools` and `tool_choice` take the app's own function tools and built-ins (including
265
270
  `image_generation`), `reasoning.effort` how hard the model thinks, and `text` how long the answer is with the shape it
@@ -355,3 +360,11 @@ For a plaintext store, stop all writers, revoke the old credentials using the ol
355
360
  remove the old app-owned credential file, and sign in again with a sealing adapter. Old plaintext
356
361
  backups may retain tokens: delete them under the host's retention policy and revoke the affected
357
362
  credentials. Do not point this migration at another tool's sign-in directory.
363
+ `classifyFailure(error, nowMs?)` returns a typed `Failure` (`{ kind, until }`) or `null`
364
+ for an unrecognized error. `until` is epoch milliseconds when the error says "try again
365
+ in N min/hours", and zero otherwise. For resting kinds, use
366
+ `failure.until || nowMs + REST_MS[failure.kind]`; signed-out, not-included and network
367
+ failures have no fallback rest. `classify` remains an alias. Pass a clock for deterministic
368
+ classification; the default uses `Date.now()`. It stores and logs no error text.
369
+
370
+ For a capability running where the sign-in lives, `await accounts.access(member, signal)` returns fresh `{ access, accountId }` from the app’s own ChatGPT store. Keep these credentials in that process; proxy signaling or relay media for another device. This uses the same refresh and signed-out behavior as `respond`.
@@ -1,6 +1,5 @@
1
1
  import type { CredentialStore, Models } from '@earendil-works/pi-ai';
2
2
  import { type Provider } from './catalogue.ts';
3
- import { type Kind } from './limits.ts';
4
3
  import { ResponseError, type Ask, type ResponseResult, type ResponseTool } from './responses.ts';
5
4
  import { type EndingStore, type RefreshStore } from './stores.ts';
6
5
  import { type Why } from './words.ts';
@@ -126,10 +125,11 @@ export declare class Accounts<R extends AuthHost = AuthHost, M extends Member =
126
125
  forget(member: M, key: string): void;
127
126
  /** 0 when the account is available; otherwise when it stops resting. */
128
127
  restingUntil(member: M, key: string): number;
129
- /** An account's error, acted on. A limit or overload rests it (until when it said, or a default). A plan without this
130
- * use is marked so. A refusal is checked: a sign-in that no longer refreshes is signed out for real, one that still
131
- * does was a passing refusal and rests a few minutes (kind `overloaded`) rather than loop. Returns the kind acted on,
132
- * or null for an error that is not about the account; `network` changes nothing. */
128
+ /** Fresh ChatGPT access for a host-side capability. Never send this to another device. */
129
+ access(member: M, signal?: AbortSignal): Promise<{
130
+ access: string;
131
+ accountId: string;
132
+ }>;
133
133
  /** Ask ChatGPT with this member's own sign-in, the answer streaming into `onText`; refreshed first when due. A
134
134
  * failure about the account (a limit, a lapsed sign-in) is acted on as `failed()` does, then thrown as a
135
135
  * ResponseError with the words to show. Without `tools` the answer is the plain text, as before: pass `input` as
@@ -141,10 +141,11 @@ export declare class Accounts<R extends AuthHost = AuthHost, M extends Member =
141
141
  respond(member: M, ask: Ask & {
142
142
  tools: ResponseTool[];
143
143
  }): Promise<ResponseResult>;
144
- failed(member: M, key: string, error: string | ResponseError): Promise<{
145
- kind: Kind;
146
- until: number;
147
- } | null>;
144
+ /** An account's error, acted on. A limit or overload rests it (until when it said, or a default). A plan without this
145
+ * use is marked so. A refusal is checked: a sign-in that no longer refreshes is signed out for real, one that still
146
+ * does was a passing refusal and rests a few minutes (kind `overloaded`) rather than loop. Returns the kind acted on,
147
+ * or null for an error that is not about the account; `network` changes nothing. */
148
+ failed(member: M, key: string, error: string | ResponseError): Promise<import("./limits.ts").Failure | null>;
148
149
  /** The first choice whose account is neither resting nor known to be unusable: the fallback ladder. */
149
150
  ladder<T>(member: M, choices: readonly T[], key?: (c: T) => string): T | undefined;
150
151
  /** Where one account stands, in one plain sentence every app shows the same way. */
package/dist/accounts.js CHANGED
@@ -228,7 +228,9 @@ export class Accounts {
228
228
  const r = this.rests.get(`${member}:${key}`);
229
229
  return r && r.until > Date.now() ? r.until : 0;
230
230
  }
231
- async respond(member, ask) {
231
+ /** Fresh ChatGPT access for a host-side capability. Never send this to another device. */
232
+ async access(member, signal) {
233
+ signal?.throwIfAborted();
232
234
  const key = 'chatgpt';
233
235
  const p = this.offer(key);
234
236
  const rt = await this.runtime(member);
@@ -247,8 +249,15 @@ export class Accounts {
247
249
  const c = await rt.readCredential(p.pi).catch(() => undefined);
248
250
  if (!access || c?.type !== 'oauth')
249
251
  throw new ResponseError(say('status.signedOut', { name: p.name }), 'signed_out');
252
+ signal?.throwIfAborted();
253
+ return { access, accountId: String(c.accountId ?? '') };
254
+ }
255
+ async respond(member, ask) {
256
+ const key = 'chatgpt';
257
+ const p = this.offer(key);
258
+ const { access, accountId } = await this.access(member);
250
259
  try {
251
- const base = { ...ask, access, accountId: String(c.accountId ?? ''), model: ask.model ?? p.models.strong, base: this.opts.apiBase, fetch: this.opts.fetch, originator: ask.originator ?? this.opts.originator };
260
+ const base = { ...ask, access, accountId, model: ask.model ?? p.models.strong, base: this.opts.apiBase, fetch: this.opts.fetch, originator: ask.originator ?? this.opts.originator };
252
261
  return ask.tools ? await respond({ ...base, tools: ask.tools }) : await respond({ ...base, tools: undefined });
253
262
  }
254
263
  catch (e) {
@@ -260,6 +269,10 @@ export class Accounts {
260
269
  throw e;
261
270
  }
262
271
  }
272
+ /** An account's error, acted on. A limit or overload rests it (until when it said, or a default). A plan without this
273
+ * use is marked so. A refusal is checked: a sign-in that no longer refreshes is signed out for real, one that still
274
+ * does was a passing refusal and rests a few minutes (kind `overloaded`) rather than loop. Returns the kind acted on,
275
+ * or null for an error that is not about the account; `network` changes nothing. */
263
276
  async failed(member, key, error) {
264
277
  const c = error instanceof ResponseError ? error.kind && { kind: error.kind, until: error.until } : classify(error);
265
278
  if (!c || c.kind === 'network')
package/dist/limits.d.ts CHANGED
@@ -2,7 +2,11 @@ export type Kind = 'rate_limit' | 'overloaded' | 'signed_out' | 'not_included' |
2
2
  /** How long an account rests when its error didn't say. */
3
3
  export declare const REST_MS: Record<Kind, number>;
4
4
  /** `until` is 0 when the error didn't say. Anything unrecognised is null: the app fails that one request. */
5
- export declare function classify(error: string): {
5
+ export type Failure = {
6
6
  kind: Kind;
7
7
  until: number;
8
- } | null;
8
+ };
9
+ /** Pure when the host passes its clock. REST_MS supplies the fallback rest duration. */
10
+ export declare function classifyFailure(error: string, nowMs?: number): Failure | null;
11
+ /** Backwards-compatible name for classifyFailure. */
12
+ export declare const classify: typeof classifyFailure;
package/dist/limits.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /** How long an account rests when its error didn't say. */
2
2
  export const REST_MS = { rate_limit: 60 * 60_000, overloaded: 5 * 60_000, signed_out: 0, not_included: 0, network: 0 };
3
- /** `until` is 0 when the error didn't say. Anything unrecognised is null: the app fails that one request. */
4
- export function classify(error) {
3
+ /** Pure when the host passes its clock. REST_MS supplies the fallback rest duration. */
4
+ export function classifyFailure(error, nowMs = Date.now()) {
5
5
  const m = /try again in ~?(\d+)\s*(min|h)/i.exec(error);
6
- const until = m ? Date.now() + Number(m[1]) * (m[2].toLowerCase() === 'h' ? 3_600_000 : 60_000) : 0;
6
+ const until = m ? nowMs + Number(m[1]) * (m[2].toLowerCase() === 'h' ? 3_600_000 : 60_000) : 0;
7
7
  if (/your plan doesn't include/i.test(error))
8
8
  return { kind: 'not_included', until };
9
9
  if (/usage limit|rate.?limit|quota|too many requests|\b429\b/i.test(error))
@@ -16,3 +16,5 @@ export function classify(error) {
16
16
  return { kind: 'network', until };
17
17
  return null;
18
18
  }
19
+ /** Backwards-compatible name for classifyFailure. */
20
+ export const classify = classifyFailure;
@@ -1,7 +1,7 @@
1
1
  export { Accounts, planOf, portable, type AccountsOptions, type AuthHost, type Loopback, type Member, type Platform, type SignIn, type Status } from './accounts.ts';
2
2
  export { PROVIDERS, offered, provider, type Billing, type Provider, type Terms } from './catalogue.ts';
3
3
  export { PORTABLE, claims, credentialOf, devicePoll, deviceStart, portableEngine, type EngineOptions, type Poll } from './engine.ts';
4
- export { REST_MS, classify, type Kind } from './limits.ts';
4
+ export { REST_MS, classify, classifyFailure, type Failure, type Kind } from './limits.ts';
5
5
  export { IncompleteError, ResponseError, isFunctionCall, limitResponse, respond, sseReader, type Ask, type ResponseFunctionCall, type ResponseInputItem, type ResponseOutputItem, type ResponseOutputMessage, type ResponseReasoning, type ResponseResult, type ResponseStreamEvent, type ResponseText, type ResponseTextFormat, type ResponseTool, type ResponseToolChoice } from './responses.ts';
6
6
  export { browserStore, memoryStore, recordStore, secureStore, RefreshRequiredError, type RefreshStore, type SecureStoreLike } from './stores.ts';
7
7
  export { WORDS, billingWords, callbackPage, clock, failure, say, signInError, type WordKey, type Why } from './words.ts';
package/dist/portable.js CHANGED
@@ -3,7 +3,7 @@
3
3
  export { Accounts, planOf, portable } from "./accounts.js";
4
4
  export { PROVIDERS, offered, provider } from "./catalogue.js";
5
5
  export { PORTABLE, claims, credentialOf, devicePoll, deviceStart, portableEngine } from "./engine.js";
6
- export { REST_MS, classify } from "./limits.js";
6
+ export { REST_MS, classify, classifyFailure } from "./limits.js";
7
7
  export { IncompleteError, ResponseError, isFunctionCall, limitResponse, respond, sseReader } from "./responses.js";
8
8
  export { browserStore, memoryStore, recordStore, secureStore, RefreshRequiredError } from "./stores.js";
9
9
  export { WORDS, billingWords, callbackPage, clock, failure, say, signInError } from "./words.js";
@@ -175,6 +175,8 @@ export type Ask = {
175
175
  model?: string;
176
176
  /** The tools the model may call. Passed, the result carries the output items next to the text. */
177
177
  tools?: ResponseTool[];
178
+ /** Whether ChatGPT may call tools in parallel. Omitted: the provider's default. */
179
+ parallelToolCalls?: boolean;
178
180
  /** Which tool the model must use. Default: whatever it wants. */
179
181
  tool_choice?: ResponseToolChoice;
180
182
  /** How hard the model thinks. Default: none. */
package/dist/responses.js CHANGED
@@ -182,6 +182,7 @@ export async function respond(o) {
182
182
  body: JSON.stringify({
183
183
  model: o.model, store: false, stream: true, instructions: o.instructions, input,
184
184
  ...(o.tools ? { tools: o.tools } : {}),
185
+ ...(o.parallelToolCalls !== undefined ? { parallel_tool_calls: o.parallelToolCalls } : {}),
185
186
  ...(o.tool_choice !== undefined ? { tool_choice: o.tool_choice } : {}),
186
187
  text: { verbosity, ...(format ? { format } : {}), ...textRest },
187
188
  reasoning: { effort, ...reasoningRest },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/accounts",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Sign in with the AI plan you already pay for, into your app's own store.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",