@byokit/accounts 0.14.0 → 0.15.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 +8 -0
- package/README.md +33 -3
- package/dist/accounts.d.ts +9 -0
- package/dist/accounts.js +7 -1
- package/dist/chatgpt-plan.d.ts +8 -0
- package/dist/portable.d.ts +2 -2
- package/dist/responses.d.ts +15 -1
- package/dist/responses.js +5 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/mock-openai.d.ts +12 -1
- package/dist/testing/mock-openai.js +16 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.15.0 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/usage 0.5.0.
|
|
8
|
+
|
|
9
|
+
- FIX: ChatGPT subscription respond() now retains reported token usage with result: true or tools, including partial results for incomplete answers.
|
|
10
|
+
- Bind an existing ChatGPT subscription login to a member-bound chatgpt() handle without exposing credentials.
|
|
11
|
+
- Script mockOpenAI answers with string, regex or function prompt matchers and optional token usage.
|
|
12
|
+
|
|
5
13
|
## 0.14.0 (2026-10-01)
|
|
6
14
|
|
|
7
15
|
- Dependency update: pins @byokit/usage 0.4.0.
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ and [`examples/pwa`](../../examples/pwa) (browser sign-in).
|
|
|
118
118
|
|
|
119
119
|
| Export | What it does |
|
|
120
120
|
|---|---|
|
|
121
|
-
| `Accounts` | Sign-in, status, sign-out, asking and limits for each member: `login`, `finished`, `status`, `plan`, `logout`, `respond`, `failed`, `ladder`, `keepFresh` |
|
|
121
|
+
| `Accounts` | Sign-in, status, sign-out, asking and limits for each member: `login`, `finished`, `status`, `plan`, `logout`, `respond`, `chatgpt`, `failed`, `ladder`, `keepFresh` |
|
|
122
122
|
| `portable`, `computer`, `loopback` | The platform `Accounts` runs on: device code with `fetch` alone, or (Node entry only) Pi's flows and the loopback listener |
|
|
123
123
|
| `memoryStore`, `fileStore`, `secureStore`, `browserStore`, `recordStore` | One store per person: in memory, a sealed 0600 file (Node entry only), Keychain/Keystore, IndexedDB, or your own load and save |
|
|
124
124
|
| `offered`, `provider`, `PROVIDERS` | The catalogue: each provider's billing, models and source |
|
|
@@ -256,12 +256,15 @@ const accounts = new Accounts({
|
|
|
256
256
|
|
|
257
257
|
`respond(member, { instructions, input, model?, onText?, signal? })` asks ChatGPT's own answers endpoint with the
|
|
258
258
|
member's sign-in, refreshed first when due, and returns the whole text (`onText` gets each piece as it streams; the
|
|
259
|
-
returned completion is authoritative).
|
|
259
|
+
returned completion is authoritative). Pass `result: true` to receive `{ text, output, usage? }` even without tools.
|
|
260
|
+
`usage` retains provider-reported `input_tokens`, `output_tokens` and native details in the same shape as the
|
|
261
|
+
Messages route; absent usage stays absent. Subscription token counts do not imply an API-key charge.
|
|
262
|
+
A limit or a lapsed sign-in is acted on as `failed()` does, then thrown as a
|
|
260
263
|
`ResponseError` with the words to show and the kind acted on. Rules: [conformance fixtures](../../fixtures/README.md).
|
|
261
264
|
|
|
262
265
|
A cut-off answer always throws `IncompleteError` (a `ResponseError` with `kind: null`), with or without tools.
|
|
263
266
|
Its `reason` preserves the provider's `incomplete_details.reason`, including `max_output_tokens` and
|
|
264
|
-
`content_filter` (`unknown` when absent). Its `result` holds the partial `{ text, output }` for apps that want to
|
|
267
|
+
`content_filter` (`unknown` when absent). Its `result` holds the partial `{ text, output, usage? }` for apps that want to
|
|
265
268
|
show it as unfinished. `onEvent` also receives `{ type: 'incomplete', reason }` before rejection; `onText` may
|
|
266
269
|
already have shown partial words. This covers `response.incomplete` events and `status: 'incomplete'` envelopes,
|
|
267
270
|
whether fetch streams SSE, buffers it, or returns JSON. The account stays signed in and is not put to rest.
|
|
@@ -332,6 +335,14 @@ tests and demos sign in and ask end to end with no account. Point the kit at it
|
|
|
332
335
|
|
|
333
336
|
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](../../NOTICE).
|
|
334
337
|
|
|
338
|
+
## ChatGPT subscription handles
|
|
339
|
+
|
|
340
|
+
After `Accounts.login(member, 'chatgpt')` finishes, `accounts.chatgpt(member)` returns a member-bound
|
|
341
|
+
subscription handle for `openai({ auth: 'account', account: handle, model })` in `@byokit/decide`.
|
|
342
|
+
It routes requests through `Accounts.respond`, which keeps tokens in the app's store and refreshes them
|
|
343
|
+
before use. The handle contains no credentials, follows sign-out, and needs no additional sign-in or
|
|
344
|
+
API key (billed per use). ChatGPT remains offered by default. Use the handle only where the sign-in lives.
|
|
345
|
+
|
|
335
346
|
## Official ChatGPT plan token-sharing adapter
|
|
336
347
|
|
|
337
348
|
`chatgptPlan({ session })`, also exported from `@byokit/accounts/chatgpt-plan`, binds a single person's
|
|
@@ -589,3 +600,22 @@ Native status reads resolve within 15 seconds. Claude stdout is capped at 256 KB
|
|
|
589
600
|
The existing `accounts-v1.json` `{version:1,accounts:[{id,provider,name,folder,found}]}` and `auto-terms-v1.json` `{acknowledged:true}` encodings remain unchanged, with 0600 files and atomic replacement. The kit preserves but excludes `found-*` and `found:true` rows, which belong to the host's default-login adapter. Legacy managed rows without kit completion sidecars retain their native signed-in status; new or re-signing rows require the completion marker. Symlinked account folders and records outside the provider/hex layout are refused.
|
|
590
601
|
|
|
591
602
|
`usageSource(id)` returns a Codex Source for `@byokit/usage`; Claude returns `undefined`, and its usage Source is `{provider:'claude', folder:set.CLAUDE_CONFIG_DIR, headers}` in a usage reader whose `stateDir` is the same managed root. `kinds` serves only the matching native agent (`claude` or `codex`); Pi is excluded until its folder mapping is verified. `resumeArgs` accepts an `id` conversation reference for these kinds. `termsAcknowledged` and `acknowledgeTerms` keep the host's existing terms bit; they do not gate sign-in. `suggestName` uses the first part of an email, falling back to the provider's name.
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
## Scripted sign-in stand-in
|
|
606
|
+
|
|
607
|
+
`mockOpenAI({ answers })` from `@byokit/accounts/testing` runs the normal device-code sign-in and
|
|
608
|
+
ChatGPT response path on loopback. Each script has `{ match, text, usage? }`: a string matches a
|
|
609
|
+
substring of joined input text, a regex tests it, and a function receives it and returns a boolean.
|
|
610
|
+
The first match wins, including on repeat requests; unmatched input retains echo/tool behavior.
|
|
611
|
+
Replace `mock.state.answers` between requests to change scripts. Counts are explicit fake data.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { mockOpenAI } from '@byokit/accounts/testing';
|
|
615
|
+
|
|
616
|
+
const mock = await mockOpenAI({ email: 'umer@example.com', answers: [
|
|
617
|
+
{ match: 'Umer', text: 'Ready.', usage: { input_tokens: 12, output_tokens: 3 } },
|
|
618
|
+
{ match: /finished/i, text: 'Done.' },
|
|
619
|
+
{ match: (prompt) => prompt.includes('help'), text: 'I can help.' },
|
|
620
|
+
] });
|
|
621
|
+
```
|
package/dist/accounts.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ClaudePlanOptions } from './claude-plan.ts';
|
|
|
3
3
|
import { type AnthropicAsk, type AnthropicResult, type AnthropicTool } from './anthropic.ts';
|
|
4
4
|
import { type Provider } from './catalogue.ts';
|
|
5
5
|
import { ResponseError, type Ask, type ResponseResult, type ResponseTool } from './responses.ts';
|
|
6
|
+
import type { ChatGPTRespondAccount } from './chatgpt-plan.ts';
|
|
6
7
|
import { type EndingStore, type RefreshStore } from './stores.ts';
|
|
7
8
|
import { type Why } from './words.ts';
|
|
8
9
|
/** What signing in needs from an engine: Pi's `Models`, or anything shaped like it (the coding agent's `ModelRuntime`). */
|
|
@@ -171,12 +172,20 @@ export declare class Accounts<R extends AuthHost = AuthHost, M extends Member =
|
|
|
171
172
|
result?: false;
|
|
172
173
|
}): Promise<string>;
|
|
173
174
|
respond(member: M, ask: AnthropicAccountAsk): Promise<string | AnthropicResult>;
|
|
175
|
+
respond(member: M, ask: Ask & {
|
|
176
|
+
result: true;
|
|
177
|
+
}): Promise<ResponseResult>;
|
|
174
178
|
respond(member: M, ask: Ask & {
|
|
175
179
|
tools?: undefined;
|
|
180
|
+
result?: false;
|
|
176
181
|
}): Promise<string>;
|
|
177
182
|
respond(member: M, ask: Ask & {
|
|
178
183
|
tools: ResponseTool[];
|
|
179
184
|
}): Promise<ResponseResult>;
|
|
185
|
+
respond(member: M, ask: Ask): Promise<string | ResponseResult>;
|
|
186
|
+
/** Bind this member's existing ChatGPT subscription login for consumers such as decide. Each request uses
|
|
187
|
+
* the current sign-in and Accounts' refresh/limit/sign-out handling; the handle exposes no credentials. */
|
|
188
|
+
chatgpt(member: M): ChatGPTRespondAccount;
|
|
180
189
|
/** An account's error, acted on. A limit or overload rests it (until when it said, or a default). A plan without this
|
|
181
190
|
* use is marked so. A refusal is checked: a sign-in that no longer refreshes is signed out for real, one that still
|
|
182
191
|
* does was a passing refusal and rests a few minutes (kind `overloaded`) rather than loop. Returns the kind acted on,
|
package/dist/accounts.js
CHANGED
|
@@ -300,7 +300,7 @@ export class Accounts {
|
|
|
300
300
|
const { access, accountId } = await this.access(member);
|
|
301
301
|
try {
|
|
302
302
|
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 };
|
|
303
|
-
return
|
|
303
|
+
return await respond(base);
|
|
304
304
|
}
|
|
305
305
|
catch (e) {
|
|
306
306
|
if (e instanceof ResponseError && e.kind && e.kind !== 'network') {
|
|
@@ -311,6 +311,12 @@ export class Accounts {
|
|
|
311
311
|
throw e;
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
|
+
/** Bind this member's existing ChatGPT subscription login for consumers such as decide. Each request uses
|
|
315
|
+
* the current sign-in and Accounts' refresh/limit/sign-out handling; the handle exposes no credentials. */
|
|
316
|
+
chatgpt(member) {
|
|
317
|
+
this.offer('chatgpt');
|
|
318
|
+
return { billing: 'subscription', respond: (ask) => this.respond(member, ask) };
|
|
319
|
+
}
|
|
314
320
|
/** An account's error, acted on. A limit or overload rests it (until when it said, or a default). A plan without this
|
|
315
321
|
* use is marked so. A refusal is checked: a sign-in that no longer refreshes is signed out for real, one that still
|
|
316
322
|
* does was a passing refusal and rests a few minutes (kind `overloaded`) rather than loop. Returns the kind acted on,
|
package/dist/chatgpt-plan.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { Ask, ResponseResult } from './responses.ts';
|
|
2
|
+
/** A member-bound Accounts ChatGPT subscription handle. Credentials stay inside Accounts. */
|
|
3
|
+
export type ChatGPTRespondAccount = {
|
|
4
|
+
readonly billing: 'subscription';
|
|
5
|
+
respond(ask: Ask & {
|
|
6
|
+
result: true;
|
|
7
|
+
}): Promise<ResponseResult>;
|
|
8
|
+
};
|
|
1
9
|
/** A host-owned, validated Sign in with ChatGPT token-sharing session. The host completes the official
|
|
2
10
|
* OAuth flow (including ID-token verification), stores it per person, and refreshes it before returning it.
|
|
3
11
|
* This adapter never discovers credentials, starts a login, or substitutes a Codex credential. */
|
package/dist/portable.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ export { Accounts, planOf, portable, type AccountsOptions, type ClaudePlanAsk, t
|
|
|
2
2
|
export { PROVIDERS, offered, provider, type Billing, type MultiAccountTerms, type Provider } from './catalogue.ts';
|
|
3
3
|
export { PORTABLE, claims, credentialOf, devicePoll, deviceStart, portableEngine, type EngineOptions, type Poll } from './engine.ts';
|
|
4
4
|
export { REST_MS, classify, classifyFailure, type Failure, type Kind } from './limits.ts';
|
|
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';
|
|
5
|
+
export { IncompleteError, ResponseError, isFunctionCall, limitResponse, respond, sseReader, type Ask, type ResponseFunctionCall, type ResponseInputItem, type ResponseOutputItem, type ResponseOutputMessage, type ResponseReasoning, type ResponseResult, type ResponseUsage, type ResponseStreamEvent, type ResponseText, type ResponseTextFormat, type ResponseTool, type ResponseToolChoice } from './responses.ts';
|
|
6
6
|
export { browserStore, keystoreStore, 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';
|
|
8
|
-
export { chatgptPlan, UnsupportedAccountError, type ChatGPTPlanAccount, type ChatGPTPlanSession } from './chatgpt-plan.ts';
|
|
8
|
+
export { chatgptPlan, UnsupportedAccountError, type ChatGPTPlanAccount, type ChatGPTRespondAccount, type ChatGPTPlanSession } from './chatgpt-plan.ts';
|
|
9
9
|
export { anthropic, anthropicSseReader, AnthropicIncompleteError, type AnthropicAsk, type AnthropicCacheControl, type AnthropicContent, type AnthropicImage, type AnthropicMessage, type AnthropicOptions, type AnthropicRequest, type AnthropicResponse, type AnthropicResult, type AnthropicStreamEvent, type AnthropicText, type AnthropicThinking, type AnthropicTool, type AnthropicToolChoice, type AnthropicToolUse, type AnthropicUsage } from './anthropic.ts';
|
|
10
10
|
export { ClaudePlanExpiredError, ClaudePlanPlatformError, claudeAuthorization, claudeCode, type ClaudePlanOptions } from './claude-plan.ts';
|
|
11
11
|
export { chooseAccount, resolveSelection, roomOf, roomWords, type AccountLike, type AccountPick, type Considered, type Defaults, type PickWhy, type Room, type RoomSpan, type RunSelection, type SignInState } from './multi.ts';
|
package/dist/responses.d.ts
CHANGED
|
@@ -128,10 +128,17 @@ export type ResponseOutputItem = ResponseOutputMessage | ResponseFunctionCall |
|
|
|
128
128
|
};
|
|
129
129
|
/** Whether an output item is a function call, so an app can answer it with a `function_call_output` turn. */
|
|
130
130
|
export declare const isFunctionCall: (item: ResponseOutputItem) => item is ResponseFunctionCall;
|
|
131
|
-
/**
|
|
131
|
+
/** Provider-reported token counts, with native details retained. Missing usage is never guessed. */
|
|
132
|
+
export type ResponseUsage = {
|
|
133
|
+
input_tokens: number;
|
|
134
|
+
output_tokens: number;
|
|
135
|
+
[key: string]: unknown;
|
|
136
|
+
};
|
|
137
|
+
/** The model's answer: text, output items and token usage when the provider reports it. */
|
|
132
138
|
export type ResponseResult = {
|
|
133
139
|
text: string;
|
|
134
140
|
output: ResponseOutputItem[];
|
|
141
|
+
usage?: ResponseUsage;
|
|
135
142
|
};
|
|
136
143
|
/** What streams besides the words: each text piece, each tool call as it builds and lands, and each output item. */
|
|
137
144
|
export type ResponseStreamEvent = {
|
|
@@ -166,6 +173,8 @@ export declare function sseReader(onText?: (delta: string) => void, onEvent?: (e
|
|
|
166
173
|
result(): ResponseResult;
|
|
167
174
|
};
|
|
168
175
|
export type Ask = {
|
|
176
|
+
/** Return text, output items and reported usage even without tools. */
|
|
177
|
+
result?: boolean;
|
|
169
178
|
/** What the model is told to be. */
|
|
170
179
|
instructions: string;
|
|
171
180
|
/** The person's words, or the turns so far: messages (with `input_image` where the person attached one) and, after a
|
|
@@ -201,10 +210,15 @@ type Access = {
|
|
|
201
210
|
/** Ask ChatGPT with a signed-in token. `fetch`: pass one that streams (Expo's `expo/fetch`); any fetch works.
|
|
202
211
|
* Without `tools` the answer is the plain text, as before; with `tools` it is the text with every output item.
|
|
203
212
|
* Incomplete answers always throw IncompleteError and notify onEvent, including with tools. */
|
|
213
|
+
export declare function respond(o: Ask & Access & {
|
|
214
|
+
result: true;
|
|
215
|
+
}): Promise<ResponseResult>;
|
|
204
216
|
export declare function respond(o: Ask & Access & {
|
|
205
217
|
tools?: undefined;
|
|
218
|
+
result?: false;
|
|
206
219
|
}): Promise<string>;
|
|
207
220
|
export declare function respond(o: Ask & Access & {
|
|
208
221
|
tools: ResponseTool[];
|
|
209
222
|
}): Promise<ResponseResult>;
|
|
223
|
+
export declare function respond(o: Ask & Access): Promise<string | ResponseResult>;
|
|
210
224
|
export {};
|
package/dist/responses.js
CHANGED
|
@@ -58,6 +58,7 @@ export function sseReader(onText, onEvent) {
|
|
|
58
58
|
let buffer = '', text = '', completed;
|
|
59
59
|
let done = false, finished;
|
|
60
60
|
let incomplete;
|
|
61
|
+
let usage;
|
|
61
62
|
const output = [];
|
|
62
63
|
const emitted = new Set();
|
|
63
64
|
const calls = new Map();
|
|
@@ -115,6 +116,8 @@ export function sseReader(onText, onEvent) {
|
|
|
115
116
|
}
|
|
116
117
|
if (e.type === 'response.completed' || e.type === 'response.incomplete' || e.response?.status === 'incomplete') {
|
|
117
118
|
done = true;
|
|
119
|
+
if (isRecord(e.response?.usage))
|
|
120
|
+
usage = { ...e.response.usage };
|
|
118
121
|
if (Array.isArray(e.response?.output)) {
|
|
119
122
|
for (const [i, item] of e.response.output.entries())
|
|
120
123
|
land(item, i);
|
|
@@ -155,7 +158,7 @@ export function sseReader(onText, onEvent) {
|
|
|
155
158
|
throw new ResponseError('ChatGPT stopped before completing its answer.', 'network');
|
|
156
159
|
if (text === '' && whole !== '')
|
|
157
160
|
onText?.(whole);
|
|
158
|
-
finished = { text: whole, output };
|
|
161
|
+
finished = { text: whole, output, ...(usage && { usage }) };
|
|
159
162
|
}
|
|
160
163
|
if (incomplete !== undefined)
|
|
161
164
|
throw new IncompleteError(incomplete, finished);
|
|
@@ -214,5 +217,5 @@ export async function respond(o) {
|
|
|
214
217
|
else {
|
|
215
218
|
reader.push(await res.text()); // a fetch that can't stream: the whole answer at once
|
|
216
219
|
}
|
|
217
|
-
return o.tools ? reader.result() : reader.end();
|
|
220
|
+
return o.tools || o.result ? reader.result() : reader.end();
|
|
218
221
|
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -18,4 +18,4 @@ export declare function decoy(root: string): {
|
|
|
18
18
|
/** Marks left by the decoy's extension or CLIs: something ran code from someone else's setup. */
|
|
19
19
|
ran: () => string[];
|
|
20
20
|
};
|
|
21
|
-
export { mockJwt, mockOpenAI, type MockOpenAIOptions } from './mock-openai.ts';
|
|
21
|
+
export { mockJwt, mockOpenAI, type MockOpenAIAnswer, type MockOpenAIOptions } from './mock-openai.ts';
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import type { ResponseUsage } from '../responses.ts';
|
|
2
|
+
/** First matching script wins. Strings match a substring; regexes and predicates inspect the joined input text.
|
|
3
|
+
* Unmatched prompts retain the echo/tool behavior. Counts are supplied explicitly, never estimated. */
|
|
4
|
+
export type MockOpenAIAnswer = {
|
|
5
|
+
match: string | RegExp | ((prompt: string) => boolean);
|
|
6
|
+
text: string;
|
|
7
|
+
usage?: ResponseUsage;
|
|
8
|
+
};
|
|
1
9
|
export type MockOpenAIOptions = {
|
|
2
10
|
port?: number;
|
|
3
11
|
host?: string;
|
|
4
12
|
plan?: string;
|
|
5
13
|
email?: string;
|
|
6
14
|
expiresIn?: number;
|
|
15
|
+
answers?: readonly MockOpenAIAnswer[];
|
|
7
16
|
log?: (line: string) => void;
|
|
8
17
|
};
|
|
9
18
|
/** An access token as OpenAI shapes it: the account, the plan and the email in its claims. */
|
|
10
19
|
export declare const mockJwt: (plan?: string, email?: string, n?: number) => string;
|
|
11
|
-
export declare function mockOpenAI({ port, host, plan, email, expiresIn, log }?: MockOpenAIOptions): Promise<{
|
|
20
|
+
export declare function mockOpenAI({ port, host, plan, email, expiresIn, answers, log }?: MockOpenAIOptions): Promise<{
|
|
12
21
|
base: string;
|
|
13
22
|
state: {
|
|
14
23
|
/** Refresh tokens OpenAI still honours; a refresh spends the old one (rotation), sign-out revokes one. */
|
|
@@ -17,6 +26,8 @@ export declare function mockOpenAI({ port, host, plan, email, expiresIn, log }?:
|
|
|
17
26
|
path: string;
|
|
18
27
|
body: string;
|
|
19
28
|
}[];
|
|
29
|
+
/** Scripts may be replaced between requests, without restarting the sign-in stand-in. */
|
|
30
|
+
answers: MockOpenAIAnswer[];
|
|
20
31
|
/** Refuse every refresh, as when the person signed out elsewhere. */
|
|
21
32
|
refuse: boolean;
|
|
22
33
|
/** Seconds each issued token lives. */
|
|
@@ -11,13 +11,15 @@ export const mockJwt = (plan = 'plus', email = 'sara@example.com', n = 0) => ['e
|
|
|
11
11
|
// As long as a real one, whose claims fill about 1.5 kB.
|
|
12
12
|
scp: ['openid', 'profile', 'email', 'offline_access'], pad: 'x'.repeat(1200),
|
|
13
13
|
})).toString('base64url'), 'sig'].join('.');
|
|
14
|
-
export async function mockOpenAI({ port = 0, host = '127.0.0.1', plan = 'plus', email = 'sara@example.com', expiresIn = 864_000, log } = {}) {
|
|
14
|
+
export async function mockOpenAI({ port = 0, host = '127.0.0.1', plan = 'plus', email = 'sara@example.com', expiresIn = 864_000, answers = [], log } = {}) {
|
|
15
15
|
const codes = new Map();
|
|
16
16
|
let issued = 0, asked = 0;
|
|
17
17
|
const state = {
|
|
18
18
|
/** Refresh tokens OpenAI still honours; a refresh spends the old one (rotation), sign-out revokes one. */
|
|
19
19
|
live: new Set(),
|
|
20
20
|
requests: [],
|
|
21
|
+
/** Scripts may be replaced between requests, without restarting the sign-in stand-in. */
|
|
22
|
+
answers: [...answers],
|
|
21
23
|
/** Refuse every refresh, as when the person signed out elsewhere. */
|
|
22
24
|
refuse: false,
|
|
23
25
|
/** Seconds each issued token lives. */
|
|
@@ -115,12 +117,20 @@ export async function mockOpenAI({ port = 0, host = '127.0.0.1', plan = 'plus',
|
|
|
115
117
|
said.push(part.text);
|
|
116
118
|
}
|
|
117
119
|
const words = said.join(' ');
|
|
120
|
+
let scripted;
|
|
121
|
+
try {
|
|
122
|
+
scripted = state.answers.find(({ match }) => typeof match === 'string' ? words.includes(match)
|
|
123
|
+
: typeof match === 'function' ? match(words) : new RegExp(match.source, match.flags).test(words));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return send(500, { error: { message: 'The stand-in could not match this prompt.' } });
|
|
127
|
+
}
|
|
118
128
|
const schema = asked.text?.format?.type === 'json_schema';
|
|
119
|
-
const text = answered !== undefined ? `You did: ${answered}`
|
|
129
|
+
const text = scripted ? scripted.text : answered !== undefined ? `You did: ${answered}`
|
|
120
130
|
: schema ? JSON.stringify({ echo: words ? `You said: ${words}` : 'You said nothing' })
|
|
121
131
|
: `You said: ${words}`;
|
|
122
132
|
const called = Array.isArray(asked.tools) ? asked.tools.filter((t) => t?.type === 'function') : [];
|
|
123
|
-
if (called.length > 0 && answered === undefined) {
|
|
133
|
+
if (!scripted && called.length > 0 && answered === undefined) {
|
|
124
134
|
// A tool turn: the model calls the first function tool, streamed as argument deltas and one finished item,
|
|
125
135
|
// then the completion with the output list. The app answers with a `function_call_output` turn next.
|
|
126
136
|
const name = String(called[0].name ?? 'tool');
|
|
@@ -140,7 +150,9 @@ export async function mockOpenAI({ port = 0, host = '127.0.0.1', plan = 'plus',
|
|
|
140
150
|
res.write(`event: response.output_text.delta\ndata: ${JSON.stringify({ type: 'response.output_text.delta', delta })}\n\n`);
|
|
141
151
|
await new Promise((r) => setTimeout(r, 5));
|
|
142
152
|
}
|
|
143
|
-
return res.end(
|
|
153
|
+
return res.end(`event: response.completed\ndata: ${JSON.stringify({ type: 'response.completed', response: {
|
|
154
|
+
status: 'completed', ...(scripted?.usage && { usage: scripted.usage }),
|
|
155
|
+
} })}\n\ndata: [DONE]\n\n`);
|
|
144
156
|
}
|
|
145
157
|
case '/oauth/revoke':
|
|
146
158
|
state.live.delete(json().token);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/accounts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@byokit/usage": "0.
|
|
55
|
+
"@byokit/usage": "0.5.0",
|
|
56
56
|
"@earendil-works/pi-ai": "0.87.1"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|