@demicodes/provider 0.10.3 → 0.12.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/README.md CHANGED
@@ -11,9 +11,21 @@ writing providers.
11
11
  `withProviderId`.
12
12
  - **HTTP helpers** — `redactSecretText`, `httpErrorCode`, `normalizeErrorCode`,
13
13
  `providerErrorFromUnknown`, `authStatusFromKey`, `httpRequestFailedEvent`.
14
+ - **Quota** — optional `Provider.quota` (`ProviderQuota` / `ProviderQuotaSnapshot`),
15
+ `createProviderQuota`, `ensureQuota`. See
16
+ [docs/provider-quota.md](../../docs/provider-quota.md).
17
+ - **Credentials** — optional `Provider.credentials` for multi-account pool + global
18
+ `setActive` (subscription CLIs). See
19
+ [docs/provider-global-credentials.md](../../docs/provider-global-credentials.md).
14
20
 
15
21
  ```ts
16
- import { defineProvider, zeroUsage } from '@demicodes/provider'
22
+ import {
23
+ defineProvider,
24
+ createProviderQuota,
25
+ ensureQuota,
26
+ type ProviderCredentials,
27
+ type ProviderQuota,
28
+ } from '@demicodes/provider'
17
29
  ```
18
30
 
19
31
  See [Add a Provider](../../docs/guides/add-a-provider.md). Part of
@@ -0,0 +1,60 @@
1
+ import { d as ProviderCredentialInfo } from "./types-Uczx0xBQ.mjs";
2
+ //#region src/credentials-pool.d.ts
3
+ interface CredentialEntryMeta {
4
+ id: string;
5
+ label: string;
6
+ detail?: string | null;
7
+ updatedAt: string;
8
+ source?: string | null;
9
+ /** Stable account key for upsert on re-import (email, accountId, entryKey, …). */
10
+ identityKey?: string | null;
11
+ }
12
+ interface FileCredentialPoolOptions {
13
+ /** Demi state root ($DEMI_HOME / ~/.demi). */
14
+ stateDir?: string;
15
+ /** Subdir under credentials/ (e.g. codex, grok-build, claude-code). */
16
+ providerKey: string;
17
+ /** Secret filename inside each entry (auth.json, oauth.json). */
18
+ secretFileName: string;
19
+ }
20
+ /**
21
+ * Demi local state root (`$DEMI_HOME` / `~/.demi`). Canonical copy — host-local
22
+ * re-exports this for its bridge layout.
23
+ */
24
+ declare function resolveDemiHome(explicit?: string): string;
25
+ declare function credentialIdFromIdentity(identityKey: string | null | undefined, fallbackLabel: string): string;
26
+ declare function newCredentialId(): string;
27
+ declare class FileCredentialPool {
28
+ readonly root: string;
29
+ readonly secretFileName: string;
30
+ constructor(options: FileCredentialPoolOptions);
31
+ entriesDir(): string;
32
+ entryDir(id: string): string;
33
+ metaPath(id: string): string;
34
+ secretPath(id: string): string;
35
+ activePath(): string;
36
+ list(): Promise<ProviderCredentialInfo[]>;
37
+ listMeta(): Promise<CredentialEntryMeta[]>;
38
+ readMeta(id: string): Promise<CredentialEntryMeta | null>;
39
+ getActiveId(): Promise<string | null>;
40
+ setActiveId(id: string): Promise<void>;
41
+ clearActive(): Promise<void>;
42
+ writeEntry(meta: CredentialEntryMeta, secretText: string): Promise<CredentialEntryMeta>;
43
+ readSecretText(id: string): Promise<string>;
44
+ remove(id: string): Promise<void>;
45
+ private tmpPath;
46
+ /**
47
+ * Serializes pool mutations across processes with a create-exclusive lock
48
+ * file; stale locks (mtime older than 30s) are removed.
49
+ */
50
+ private withWriteLock;
51
+ findByIdentityKey(identityKey: string): Promise<CredentialEntryMeta | null>;
52
+ /** If active missing but entries exist, pick first and repair active pointer. */
53
+ ensureActivePointer(): Promise<string | null>;
54
+ }
55
+ declare class CredentialPoolError extends Error {
56
+ readonly code: 'credential_not_found' | 'credential_invalid';
57
+ constructor(code: 'credential_not_found' | 'credential_invalid', message: string);
58
+ }
59
+ //#endregion
60
+ export { CredentialEntryMeta, CredentialPoolError, FileCredentialPool, FileCredentialPoolOptions, credentialIdFromIdentity, newCredentialId, resolveDemiHome };
@@ -0,0 +1,206 @@
1
+ import { errorCode, isRecord, nonEmptyString } from "@demicodes/utils";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ //#region src/credentials-pool.ts
7
+ /**
8
+ * Demi multi-credential pool on disk:
9
+ * <stateDir>/credentials/<providerKey>/{active,entries/<id>/{meta.json,secret}}
10
+ */
11
+ /**
12
+ * Demi local state root (`$DEMI_HOME` / `~/.demi`). Canonical copy — host-local
13
+ * re-exports this for its bridge layout.
14
+ */
15
+ function resolveDemiHome(explicit) {
16
+ if (explicit && explicit.trim()) return resolve(explicit.trim());
17
+ const fromEnv = process.env.DEMI_HOME;
18
+ if (fromEnv && fromEnv.trim()) return resolve(fromEnv.trim());
19
+ return join(homedir(), ".demi");
20
+ }
21
+ function credentialIdFromIdentity(identityKey, fallbackLabel) {
22
+ const basis = nonEmptyString(identityKey) ?? fallbackLabel;
23
+ return `cred-${createHash("sha256").update(basis).digest("hex").slice(0, 16)}`;
24
+ }
25
+ function newCredentialId() {
26
+ return `cred-${randomUUID().replace(/-/g, "").slice(0, 16)}`;
27
+ }
28
+ var FileCredentialPool = class {
29
+ root;
30
+ secretFileName;
31
+ constructor(options) {
32
+ const stateDir = resolveDemiHome(options.stateDir);
33
+ this.root = join(stateDir, "credentials", options.providerKey);
34
+ this.secretFileName = options.secretFileName;
35
+ }
36
+ entriesDir() {
37
+ return join(this.root, "entries");
38
+ }
39
+ entryDir(id) {
40
+ return join(this.entriesDir(), id);
41
+ }
42
+ metaPath(id) {
43
+ return join(this.entryDir(id), "meta.json");
44
+ }
45
+ secretPath(id) {
46
+ return join(this.entryDir(id), this.secretFileName);
47
+ }
48
+ activePath() {
49
+ return join(this.root, "active");
50
+ }
51
+ async list() {
52
+ return (await this.listMeta()).map((m) => ({
53
+ id: m.id,
54
+ label: m.label,
55
+ detail: m.detail ?? null,
56
+ updatedAt: m.updatedAt
57
+ }));
58
+ }
59
+ async listMeta() {
60
+ let names;
61
+ try {
62
+ names = await readdir(this.entriesDir());
63
+ } catch {
64
+ return [];
65
+ }
66
+ const out = [];
67
+ for (const name of names) {
68
+ const meta = await this.readMeta(name);
69
+ if (meta) out.push(meta);
70
+ }
71
+ out.sort((a, b) => a.id.localeCompare(b.id));
72
+ return out;
73
+ }
74
+ async readMeta(id) {
75
+ try {
76
+ const raw = JSON.parse(await readFile(this.metaPath(id), "utf8"));
77
+ if (!isRecord(raw)) return null;
78
+ const entryId = nonEmptyString(raw.id) ?? id;
79
+ const label = nonEmptyString(raw.label);
80
+ if (!label) return null;
81
+ return {
82
+ id: entryId,
83
+ label,
84
+ detail: nonEmptyString(raw.detail) ?? null,
85
+ updatedAt: nonEmptyString(raw.updatedAt) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
86
+ source: nonEmptyString(raw.source) ?? null,
87
+ identityKey: nonEmptyString(raw.identityKey) ?? null
88
+ };
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ async getActiveId() {
94
+ try {
95
+ const id = (await readFile(this.activePath(), "utf8")).trim();
96
+ if (!id) return null;
97
+ return await this.readMeta(id) ? id : null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ async setActiveId(id) {
103
+ if (!await this.readMeta(id)) throw new CredentialPoolError("credential_not_found", `Credential "${id}" not found`);
104
+ try {
105
+ await readFile(this.secretPath(id), "utf8");
106
+ } catch {
107
+ throw new CredentialPoolError("credential_not_found", `Credential "${id}" has no secret material`);
108
+ }
109
+ await this.withWriteLock(async () => {
110
+ const tmp = this.tmpPath(this.activePath());
111
+ await writeFile(tmp, `${id}\n`, { mode: 384 });
112
+ await rename(tmp, this.activePath());
113
+ });
114
+ }
115
+ async clearActive() {
116
+ await rm(this.activePath(), { force: true }).catch(() => void 0);
117
+ }
118
+ async writeEntry(meta, secretText) {
119
+ return this.withWriteLock(async () => {
120
+ const dir = this.entryDir(meta.id);
121
+ await mkdir(dir, {
122
+ recursive: true,
123
+ mode: 448
124
+ });
125
+ const secretTmp = this.tmpPath(this.secretPath(meta.id));
126
+ const metaTmp = this.tmpPath(this.metaPath(meta.id));
127
+ await writeFile(secretTmp, secretText, { mode: 384 });
128
+ await chmod(secretTmp, 384).catch(() => void 0);
129
+ await rename(secretTmp, this.secretPath(meta.id));
130
+ await writeFile(metaTmp, `${JSON.stringify(meta, null, 2)}\n`, { mode: 384 });
131
+ await rename(metaTmp, this.metaPath(meta.id));
132
+ return meta;
133
+ });
134
+ }
135
+ async readSecretText(id) {
136
+ return readFile(this.secretPath(id), "utf8");
137
+ }
138
+ async remove(id) {
139
+ await this.withWriteLock(async () => {
140
+ const active = await this.getActiveId();
141
+ await rm(this.entryDir(id), {
142
+ recursive: true,
143
+ force: true
144
+ });
145
+ if (active === id) await this.clearActive();
146
+ });
147
+ }
148
+ tmpPath(target) {
149
+ return `${target}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
150
+ }
151
+ /**
152
+ * Serializes pool mutations across processes with a create-exclusive lock
153
+ * file; stale locks (mtime older than 30s) are removed.
154
+ */
155
+ async withWriteLock(fn) {
156
+ await mkdir(this.root, {
157
+ recursive: true,
158
+ mode: 448
159
+ });
160
+ const lockPath = join(this.root, ".lock");
161
+ const started = Date.now();
162
+ while (true) try {
163
+ await writeFile(lockPath, `${process.pid}\n`, {
164
+ flag: "wx",
165
+ mode: 384
166
+ });
167
+ break;
168
+ } catch (error) {
169
+ if (errorCode(error) !== "EEXIST") throw error;
170
+ const info = await stat(lockPath).catch(() => null);
171
+ if (info && Date.now() - info.mtimeMs > 3e4) {
172
+ await rm(lockPath, { force: true }).catch(() => void 0);
173
+ continue;
174
+ }
175
+ if (Date.now() - started > 5e3) throw new CredentialPoolError("credential_invalid", `Timed out waiting for credential pool lock ${lockPath}`);
176
+ await new Promise((resolve) => setTimeout(resolve, 25));
177
+ }
178
+ try {
179
+ return await fn();
180
+ } finally {
181
+ await rm(lockPath, { force: true }).catch(() => void 0);
182
+ }
183
+ }
184
+ async findByIdentityKey(identityKey) {
185
+ return (await this.listMeta()).find((m) => m.identityKey === identityKey) ?? null;
186
+ }
187
+ /** If active missing but entries exist, pick first and repair active pointer. */
188
+ async ensureActivePointer() {
189
+ const active = await this.getActiveId();
190
+ if (active) return active;
191
+ const all = await this.listMeta();
192
+ if (all.length === 0) return null;
193
+ await this.setActiveId(all[0].id);
194
+ return all[0].id;
195
+ }
196
+ };
197
+ var CredentialPoolError = class extends Error {
198
+ code;
199
+ constructor(code, message) {
200
+ super(message);
201
+ this.code = code;
202
+ this.name = "CredentialPoolError";
203
+ }
204
+ };
205
+ //#endregion
206
+ export { CredentialPoolError, FileCredentialPool, credentialIdFromIdentity, newCredentialId, resolveDemiHome };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,12 @@
1
- import { A as ProviderQuotaSnapshot, B as usedPercentFromRatio, C as ProviderQuotaCapability, D as ProviderQuotaProbeOptions, E as ProviderQuotaProbeCost, F as clampUsedPercent, I as createProviderQuota, L as ensureQuota, M as ProviderQuotaUnsupportedError, N as ProviderQuotaWindow, O as ProviderQuotaProbeResult, P as ProviderQuotaWindowUnit, R as severityFromUsedPercent, S as ProviderQuota, T as ProviderQuotaPlan, _ as ProviderSelection, a as ModelPolicy, b as CreateProviderQuotaOptions, c as ProviderAuthState, d as ProviderModel, f as ProviderModelCost, g as ProviderRuntimeState, h as ProviderRuntimeFactory, i as InferenceSteer, j as ProviderQuotaSource, k as ProviderQuotaSeverity, l as ProviderEvent, m as ProviderRun, n as InferenceItem, o as Provider, p as ProviderModelList, r as InferenceRequest, s as ProviderAuth, t as AgentProvider, u as ProviderFactoryDefinition, v as ProviderServiceTier, w as ProviderQuotaObserveInput, x as EnsureQuotaOptions, y as ToolDefinition, z as unixSecondsToIso } from "./types-jho3Wgsx.mjs";
2
- import { FileExtension, ModelSelection, ThinkingCapability, ThinkingConfig } from "@demicodes/core";
3
-
1
+ import { A as ProviderQuota, B as ProviderQuotaUnsupportedError, C as ProviderRuntimeFactory, D as ToolDefinition, E as ProviderServiceTier, F as ProviderQuotaProbeOptions, G as ensureQuota, H as ProviderQuotaWindowUnit, I as ProviderQuotaProbeResult, J as usedPercentFromRatio, K as severityFromUsedPercent, L as ProviderQuotaSeverity, M as ProviderQuotaObserveInput, N as ProviderQuotaPlan, O as CreateProviderQuotaOptions, P as ProviderQuotaProbeCost, R as ProviderQuotaSnapshot, S as ProviderRun, T as ProviderSelection, U as clampUsedPercent, V as ProviderQuotaWindow, W as createProviderQuota, _ as ProviderEvent, a as ModelPolicy, b as ProviderModelCost, c as ProviderAuthState, d as ProviderCredentialInfo, f as ProviderCredentialLoginOptions, g as ProviderCredentialsCapability, h as ProviderCredentials, i as InferenceSteer, j as ProviderQuotaCapability, k as EnsureQuotaOptions, l as ProviderCredentialActive, m as ProviderCredentialLoginResult, n as InferenceItem, o as Provider, p as ProviderCredentialLoginPending, q as unixSecondsToIso, r as InferenceRequest, s as ProviderAuth, t as AgentProvider, u as ProviderCredentialAddInput, v as ProviderFactoryDefinition, w as ProviderRuntimeState, x as ProviderModelList, y as ProviderModel, z as ProviderQuotaSource } from "./types-Uczx0xBQ.mjs";
2
+ import { FileExtension, ModelSelection, ThinkingCapability, ThinkingConfig, ToolResultContentBlock } from "@demicodes/core";
3
+ //#region src/content.d.ts
4
+ /**
5
+ * Flattens a tool result to plain text for wire formats without native media
6
+ * blocks; non-text blocks become `[<type>:<mediaType>]` placeholders.
7
+ */
8
+ declare function toolResultContentToText(output: ToolResultContentBlock[]): string;
9
+ //#endregion
4
10
  //#region src/provider.d.ts
5
11
  declare function defineProvider(definition: ProviderFactoryDefinition): Provider;
6
12
  declare function providerRuntime(provider: Provider, selection: ProviderSelection): Promise<AgentProvider> | AgentProvider;
@@ -41,6 +47,8 @@ type HeadersResolver = () => Record<string, string> | Promise<Record<string, str
41
47
  declare function clampPromptCacheKey(value: string): string;
42
48
  /** Replaces every occurrence of `secret` in `value` with a redaction marker. */
43
49
  declare function redactSecretText(value: string, secret: string | null | undefined): string;
50
+ /** Masks bearer tokens and named credential fields in free-form auth error text. */
51
+ declare function redactCredentialText(text: string, extraFieldPatterns?: readonly string[]): string;
44
52
  /** Maps an HTTP status (and response text) to a coarse provider error code. */
45
53
  declare function httpErrorCode(status: number, message: string): string | null;
46
54
  /** Classifies a provider error code/message into a coarse category, falling back to `code`. */
@@ -51,7 +59,9 @@ declare function providerErrorFromUnknown(error: unknown, secret: string | null
51
59
  declare function authStatusFromKey(resolveKey: SecretResolver, resolveHeaders: HeadersResolver | undefined, authHeader: string, providerLabel: string): Promise<ProviderAuthState>;
52
60
  /** Parses a Retry-After header (delta-seconds or HTTP-date) into milliseconds. */
53
61
  declare function retryAfterMsFromHeader(value: string | null): number | undefined;
62
+ /** Reads a header as a finite number, or null when absent/blank/non-numeric. */
63
+ declare function numberHeader(headers: Headers, name: string): number | null;
54
64
  /** Builds a redacted provider `error` event from a failed HTTP response. */
55
65
  declare function httpRequestFailedEvent(response: Response, secret: string | null | undefined, providerLabel: string): Promise<ProviderEvent>;
56
66
  //#endregion
57
- export { AgentProvider, type CreateProviderQuotaOptions, DEFAULT_ATTACHMENT_EXTENSIONS, type EnsureQuotaOptions, InferenceItem, InferenceRequest, InferenceSteer, ModelPolicy, type ModelSelectionFromCatalogOptions, Provider, ProviderAuth, ProviderAuthState, ProviderEvent, ProviderFactoryDefinition, ProviderModel, ProviderModelCost, ProviderModelList, type ProviderQuota, type ProviderQuotaCapability, type ProviderQuotaObserveInput, type ProviderQuotaPlan, type ProviderQuotaProbeCost, type ProviderQuotaProbeOptions, type ProviderQuotaProbeResult, type ProviderQuotaSeverity, type ProviderQuotaSnapshot, type ProviderQuotaSource, ProviderQuotaUnsupportedError, type ProviderQuotaWindow, type ProviderQuotaWindowUnit, ProviderRun, ProviderRuntimeFactory, ProviderRuntimeState, ProviderSelection, ProviderServiceTier, ToolDefinition, applyModelPolicy, authStatusFromKey, clampPromptCacheKey, clampUsedPercent, createProviderQuota, defineProvider, ensureQuota, httpErrorCode, httpRequestFailedEvent, modelSelectionFromCatalog, normalizeErrorCode, providerErrorFromUnknown, providerRuntime, redactSecretText, retryAfterMsFromHeader, severityFromUsedPercent, thinkingCapabilitiesFromProviderModel, unixSecondsToIso, usedPercentFromRatio, withProviderId };
67
+ export { AgentProvider, type CreateProviderQuotaOptions, DEFAULT_ATTACHMENT_EXTENSIONS, type EnsureQuotaOptions, InferenceItem, InferenceRequest, InferenceSteer, ModelPolicy, type ModelSelectionFromCatalogOptions, Provider, ProviderAuth, ProviderAuthState, ProviderCredentialActive, ProviderCredentialAddInput, ProviderCredentialInfo, ProviderCredentialLoginOptions, ProviderCredentialLoginPending, ProviderCredentialLoginResult, ProviderCredentials, ProviderCredentialsCapability, ProviderEvent, ProviderFactoryDefinition, ProviderModel, ProviderModelCost, ProviderModelList, type ProviderQuota, type ProviderQuotaCapability, type ProviderQuotaObserveInput, type ProviderQuotaPlan, type ProviderQuotaProbeCost, type ProviderQuotaProbeOptions, type ProviderQuotaProbeResult, type ProviderQuotaSeverity, type ProviderQuotaSnapshot, type ProviderQuotaSource, ProviderQuotaUnsupportedError, type ProviderQuotaWindow, type ProviderQuotaWindowUnit, ProviderRun, ProviderRuntimeFactory, ProviderRuntimeState, ProviderSelection, ProviderServiceTier, ToolDefinition, applyModelPolicy, authStatusFromKey, clampPromptCacheKey, clampUsedPercent, createProviderQuota, defineProvider, ensureQuota, httpErrorCode, httpRequestFailedEvent, modelSelectionFromCatalog, normalizeErrorCode, numberHeader, providerErrorFromUnknown, providerRuntime, redactCredentialText, redactSecretText, retryAfterMsFromHeader, severityFromUsedPercent, thinkingCapabilitiesFromProviderModel, toolResultContentToText, unixSecondsToIso, usedPercentFromRatio, withProviderId };
package/dist/index.mjs CHANGED
@@ -1,4 +1,14 @@
1
+ import { VIDEO_FILE_EXTENSIONS } from "@demicodes/core";
1
2
  import { shortHash } from "@demicodes/utils";
3
+ //#region src/content.ts
4
+ /**
5
+ * Flattens a tool result to plain text for wire formats without native media
6
+ * blocks; non-text blocks become `[<type>:<mediaType>]` placeholders.
7
+ */
8
+ function toolResultContentToText(output) {
9
+ return output.map((block) => block.type === "text" ? block.text : `[${block.type}:${block.source.mediaType}]`).join("\n");
10
+ }
11
+ //#endregion
2
12
  //#region src/provider.ts
3
13
  const runtimeFactorySymbol = Symbol("demi.provider.runtimeFactory");
4
14
  function defineProvider(definition) {
@@ -89,7 +99,7 @@ function modelSelectionFromCatalog(providerId, model, options = {}) {
89
99
  contextWindow: model?.contextWindow ?? 0,
90
100
  inputLimit: null,
91
101
  thinking: thinkingCapabilitiesFromProviderModel(model),
92
- acceptedExtensions: model?.supportsAttachments ? [...accepted] : []
102
+ acceptedExtensions: [...model?.supportsAttachments ? accepted : [], ...model?.supportsVideo ? VIDEO_FILE_EXTENSIONS : []]
93
103
  },
94
104
  thinking: options.thinking ?? null,
95
105
  serviceTierId: options.serviceTierId ?? null
@@ -108,10 +118,21 @@ function clampPromptCacheKey(value) {
108
118
  function redactSecretText(value, secret) {
109
119
  return secret ? value.split(secret).join("[redacted]") : value;
110
120
  }
121
+ /** Masks bearer tokens and named credential fields in free-form auth error text. */
122
+ function redactCredentialText(text, extraFieldPatterns = []) {
123
+ const fields = [
124
+ "access_token",
125
+ "refresh_token",
126
+ "id_token",
127
+ ...extraFieldPatterns
128
+ ];
129
+ return text.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/g, "Bearer [REDACTED]").replace(new RegExp(`(${fields.join("|")})["'=:\\s]+[A-Za-z0-9._~+/=-]+`, "gi"), "$1=[REDACTED]");
130
+ }
111
131
  /** Maps an HTTP status (and response text) to a coarse provider error code. */
112
132
  function httpErrorCode(status, message) {
113
133
  if (status === 401 || status === 403) return "auth_expired";
114
- if (status === 408 || status === 409 || status === 425 || status === 429 || status >= 500) return "rate_limit";
134
+ if (status === 429) return "rate_limit";
135
+ if (status === 408 || status === 409 || status === 425 || status >= 500) return "overloaded";
115
136
  if (status === 400 && /context|too long|token/i.test(message)) return "context_length_exceeded";
116
137
  return null;
117
138
  }
@@ -119,9 +140,9 @@ function httpErrorCode(status, message) {
119
140
  function normalizeErrorCode(code, message) {
120
141
  const value = `${code ?? ""} ${message}`.toLowerCase();
121
142
  if (/context|too long|max.*token/.test(value)) return "context_length_exceeded";
122
- if (/rate|quota|billing|limit/.test(value)) return "rate_limit";
123
- if (/auth|unauth|invalid.*key|expired/.test(value)) return "auth_expired";
124
- if (/overload|unavailable|timeout/.test(value)) return "overloaded";
143
+ if (/rate|quota|usage|billing|balance|limit/.test(value)) return "rate_limit";
144
+ if (/\bauth(?:entication|orization)?\b/.test(value) || /(?:invalid|expired).*(?:api|access|auth)[_\s-]*(?:key|token)/.test(value) || /(?:api|access|auth)[_\s-]*(?:key|token).*(?:invalid|expired)/.test(value)) return "auth_expired";
145
+ if (/overload|unavailable|(?:server|internal|api)[_\s-]*error|timed?\s?out|fetch failed|network|socket|econn/.test(value)) return "overloaded";
125
146
  return code;
126
147
  }
127
148
  /** Builds a provider `error` event from an unknown thrown value, redacting `secret`. */
@@ -150,6 +171,13 @@ function retryAfterMsFromHeader(value) {
150
171
  const dateMs = Date.parse(value);
151
172
  if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
152
173
  }
174
+ /** Reads a header as a finite number, or null when absent/blank/non-numeric. */
175
+ function numberHeader(headers, name) {
176
+ const raw = headers.get(name);
177
+ if (raw == null || raw === "") return null;
178
+ const n = Number(raw);
179
+ return Number.isFinite(n) ? n : null;
180
+ }
153
181
  /** Builds a redacted provider `error` event from a failed HTTP response. */
154
182
  async function httpRequestFailedEvent(response, secret, providerLabel) {
155
183
  const text = await response.text().catch(() => "");
@@ -207,13 +235,14 @@ function createProviderQuota(options) {
207
235
  };
208
236
  };
209
237
  const materialize = (partial, source) => {
238
+ const previous = source === "observation" ? latest : null;
210
239
  const snapshot = {
211
240
  providerId: options.providerId,
212
241
  observedAt: (/* @__PURE__ */ new Date()).toISOString(),
213
242
  source,
214
- plan: partial.plan ?? null,
215
- accountLabel: partial.accountLabel ?? null,
216
- windows: partial.windows,
243
+ plan: partial.plan === void 0 ? previous?.plan ?? null : partial.plan,
244
+ accountLabel: partial.accountLabel === void 0 ? previous?.accountLabel ?? null : partial.accountLabel,
245
+ windows: previous ? mergeQuotaWindows(previous.windows, partial.windows) : partial.windows,
217
246
  raw: partial.raw
218
247
  };
219
248
  latest = snapshot;
@@ -222,9 +251,13 @@ function createProviderQuota(options) {
222
251
  const quota = {
223
252
  capability,
224
253
  latest: () => latest,
254
+ clearLatest: () => {
255
+ latest = null;
256
+ },
225
257
  async probe(probeOptions = {}) {
226
258
  if (!options.canProbe) throw new ProviderQuotaUnsupportedError(options.providerId);
227
- return materialize(await options.probe(probeOptions), "probe");
259
+ const partial = await options.probe(probeOptions);
260
+ return materialize(partial, "probe");
228
261
  }
229
262
  };
230
263
  if (options.observe) {
@@ -237,6 +270,13 @@ function createProviderQuota(options) {
237
270
  }
238
271
  return quota;
239
272
  }
273
+ function mergeQuotaWindows(previous, observed) {
274
+ const observedById = new Map(observed.map((window) => [window.id, window]));
275
+ const merged = previous.map((window) => observedById.get(window.id) ?? window);
276
+ const previousIds = new Set(previous.map((window) => window.id));
277
+ for (const window of observed) if (!previousIds.has(window.id)) merged.push(window);
278
+ return merged;
279
+ }
240
280
  /** Clamp percent into 0–100 or null. */
241
281
  function clampUsedPercent(value) {
242
282
  if (typeof value !== "number" || !Number.isFinite(value)) return null;
@@ -269,4 +309,4 @@ function severityFromUsedPercent(usedPercent) {
269
309
  return "normal";
270
310
  }
271
311
  //#endregion
272
- export { DEFAULT_ATTACHMENT_EXTENSIONS, ProviderQuotaUnsupportedError, applyModelPolicy, authStatusFromKey, clampPromptCacheKey, clampUsedPercent, createProviderQuota, defineProvider, ensureQuota, httpErrorCode, httpRequestFailedEvent, modelSelectionFromCatalog, normalizeErrorCode, providerErrorFromUnknown, providerRuntime, redactSecretText, retryAfterMsFromHeader, severityFromUsedPercent, thinkingCapabilitiesFromProviderModel, unixSecondsToIso, usedPercentFromRatio, withProviderId };
312
+ export { DEFAULT_ATTACHMENT_EXTENSIONS, ProviderQuotaUnsupportedError, applyModelPolicy, authStatusFromKey, clampPromptCacheKey, clampUsedPercent, createProviderQuota, defineProvider, ensureQuota, httpErrorCode, httpRequestFailedEvent, modelSelectionFromCatalog, normalizeErrorCode, numberHeader, providerErrorFromUnknown, providerRuntime, redactCredentialText, redactSecretText, retryAfterMsFromHeader, severityFromUsedPercent, thinkingCapabilitiesFromProviderModel, toolResultContentToText, unixSecondsToIso, usedPercentFromRatio, withProviderId };
@@ -1,5 +1,4 @@
1
- import { i as InferenceSteer, l as ProviderEvent, m as ProviderRun, r as InferenceRequest, t as AgentProvider } from "./types-jho3Wgsx.mjs";
2
-
1
+ import { S as ProviderRun, _ as ProviderEvent, i as InferenceSteer, r as InferenceRequest, t as AgentProvider } from "./types-Uczx0xBQ.mjs";
3
2
  //#region src/testing.d.ts
4
3
  /**
5
4
  * Scripted provider for testing. Each "turn" is a list of events to yield.
@@ -20,6 +19,7 @@ declare class StubProvider implements AgentProvider {
20
19
  private turns;
21
20
  private cursor;
22
21
  constructor(turns: TurnScript[]);
22
+ clone(): AgentProvider;
23
23
  run(request: InferenceRequest): AsyncIterable<ProviderEvent>;
24
24
  /** Number of turns consumed so far. Useful for assertions. */
25
25
  get consumedTurns(): number;
package/dist/testing.mjs CHANGED
@@ -20,6 +20,12 @@ var StubProvider = class {
20
20
  constructor(turns) {
21
21
  this.turns = turns;
22
22
  }
23
+ clone() {
24
+ return {
25
+ run: (request) => this.run(request),
26
+ clone: () => this.clone()
27
+ };
28
+ }
23
29
  async *run(request) {
24
30
  const turn = this.turns[this.cursor];
25
31
  this.cursor += 1;
@@ -1,5 +1,4 @@
1
- import { ModelSelection, ThinkingConfig, ThinkingEffort, TokenUsage, ToolResultContentBlock, UserContentBlock } from "@demicodes/core";
2
-
1
+ import { ModelSelection, ProviderErrorDiagnostics, ThinkingConfig, ThinkingEffort, TokenUsage, ToolResultContentBlock, UserContentBlock } from "@demicodes/core";
3
2
  //#region src/quota.d.ts
4
3
  /**
5
4
  * Unified subscription / rate-limit quota surface for concrete providers.
@@ -50,7 +49,8 @@ type ProviderQuotaCapability = {
50
49
  } | {
51
50
  mode: 'supported';
52
51
  canProbe: boolean;
53
- canObserve: boolean; /** free = dedicated usage API; minimal_request = burns a tiny inference */
52
+ canObserve: boolean;
53
+ /** free = dedicated usage API; minimal_request = burns a tiny inference */
54
54
  probeCost?: ProviderQuotaProbeCost;
55
55
  staleAfterMs?: number;
56
56
  };
@@ -73,6 +73,11 @@ interface ProviderQuota {
73
73
  capability(): ProviderQuotaCapability;
74
74
  probe(options?: ProviderQuotaProbeOptions): Promise<ProviderQuotaSnapshot>;
75
75
  latest(): ProviderQuotaSnapshot | null;
76
+ /**
77
+ * Drop the in-memory latest snapshot (e.g. after credentials.setActive so the
78
+ * next ensureQuota/probe does not show the previous account).
79
+ */
80
+ clearLatest?(): void;
76
81
  /**
77
82
  * Optional passive update from a vendor HTTP response (headers, etc.).
78
83
  * Returns the new snapshot when observation succeeded; otherwise null.
@@ -201,13 +206,24 @@ type ProviderEvent = {
201
206
  toolUseId: string;
202
207
  toolName: string;
203
208
  input: unknown;
204
- } | {
209
+ } |
210
+ /**
211
+ * End of a model response. `usage` MUST be the usage of a single API request
212
+ * — the final one when the provider made several internally (e.g. a CLI turn
213
+ * with tool calls). The agent anchors its context-size estimation on it
214
+ * (input + output + cache reads/writes ≈ the context the next request will
215
+ * carry), so a turn-cumulative total here inflates the estimate and triggers
216
+ * spurious compaction.
217
+ */
218
+ {
205
219
  type: 'response';
206
220
  usage: TokenUsage;
207
221
  } | {
208
222
  type: 'error';
209
223
  message: string;
210
- code: string | null; /** Server-suggested retry delay (e.g. from a Retry-After header), if any. */
224
+ code: string | null;
225
+ diagnostics?: ProviderErrorDiagnostics;
226
+ /** Server-suggested retry delay (e.g. from a Retry-After header), if any. */
211
227
  retryAfterMs?: number;
212
228
  } | {
213
229
  type: 'abort';
@@ -223,6 +239,14 @@ interface ProviderRun extends AsyncIterable<ProviderEvent> {
223
239
  }
224
240
  interface AgentProvider {
225
241
  run(request: InferenceRequest): ProviderRun;
242
+ /**
243
+ * Creates an independent runtime with the same provider configuration.
244
+ *
245
+ * Configuration dependencies such as auth stores and quota observers may be
246
+ * shared, but mutable per-session execution state (subprocesses, sockets,
247
+ * pending tool calls, and continuation state) must not be shared.
248
+ */
249
+ clone(): AgentProvider;
226
250
  /**
227
251
  * Releases any resources the provider holds open across turns — e.g. a long-lived CLI
228
252
  * subprocess kept alive for a whole session. Called once when the owning session closes.
@@ -249,6 +273,113 @@ type ProviderAuthState = {
249
273
  interface ProviderAuth {
250
274
  status(): Promise<ProviderAuthState> | ProviderAuthState;
251
275
  }
276
+ /** Public metadata only — never tokens, cookies, or raw auth files. */
277
+ interface ProviderCredentialInfo {
278
+ /** Stable id within this provider (not globally unique across providers). */
279
+ id: string;
280
+ /** Human label: email, account id, or import tag. */
281
+ label: string;
282
+ /** Optional secondary display (plan name, issuer, …). */
283
+ detail?: string | null;
284
+ /** ISO-8601 when this entry was last imported or refreshed in the pool. */
285
+ updatedAt?: string | null;
286
+ }
287
+ interface ProviderCredentialActive {
288
+ credentialId: string | null;
289
+ /** Same shape as auth status, for the active credential. */
290
+ status: ProviderAuthState;
291
+ }
292
+ /**
293
+ * User-facing material issued mid-flow by a device-code login: the product
294
+ * relays it to the user, who completes login from any browser on any device.
295
+ */
296
+ interface ProviderCredentialLoginPending {
297
+ /** URL the user opens to confirm the login. */
298
+ verificationUrl: string;
299
+ /** One-time code the user enters at the verification URL (device-code flows). */
300
+ userCode?: string | null;
301
+ /** ISO-8601 expiry of the code, when the vendor exposes one. */
302
+ expiresAt?: string | null;
303
+ /**
304
+ * True when the vendor displays a code AFTER approval that the user must
305
+ * bring back; the product collects it via `promptForCode`.
306
+ */
307
+ requiresCodeInput?: boolean;
308
+ }
309
+ interface ProviderCredentialLoginOptions {
310
+ /** Abort the login flow. */
311
+ signal?: AbortSignal;
312
+ /** Fires once when the flow issues user-facing material (device-code login). */
313
+ onPending?: (pending: ProviderCredentialLoginPending) => void;
314
+ /** Collects the code the user copied back from the vendor page (`requiresCodeInput` flows). */
315
+ promptForCode?: () => Promise<string>;
316
+ }
317
+ /**
318
+ * Login invoke result. Every login flow runs the vendor's public protocol
319
+ * natively, imports the resulting material into the pool, and returns the
320
+ * pool `credentialId`.
321
+ */
322
+ type ProviderCredentialLoginResult = {
323
+ status: 'completed';
324
+ credentialId: string;
325
+ } | {
326
+ status: 'cancelled';
327
+ } | {
328
+ status: 'unavailable';
329
+ message: string;
330
+ } | {
331
+ status: 'failed';
332
+ message: string;
333
+ };
334
+ /**
335
+ * Provider-specific add payloads. Concrete packages document accepted variants.
336
+ * Do not put secrets on browser-visible control protocols.
337
+ */
338
+ type ProviderCredentialAddInput = {
339
+ [key: string]: unknown;
340
+ };
341
+ type ProviderCredentialsCapability = {
342
+ mode: 'none';
343
+ } | {
344
+ mode: 'supported';
345
+ /** Can run the vendor's native login flow (`beginLogin`). */
346
+ canBeginLogin?: boolean;
347
+ /** Can import from the vendor default location into the pool. */
348
+ canImportDefault?: boolean;
349
+ /** Can register externally supplied material (`add`). */
350
+ canAdd?: boolean;
351
+ /** Pool can hold more than one credential. */
352
+ multi?: boolean;
353
+ };
354
+ /**
355
+ * Multi-credential pool + process-global active switch.
356
+ * See `docs/provider-global-credentials.md`.
357
+ */
358
+ interface ProviderCredentials {
359
+ capability(): ProviderCredentialsCapability;
360
+ list(): Promise<ProviderCredentialInfo[]> | ProviderCredentialInfo[];
361
+ getActive(): Promise<ProviderCredentialActive> | ProviderCredentialActive;
362
+ /**
363
+ * Make `credentialId` the process-global active credential for this provider.
364
+ * Subsequent auth / quota / inference use it.
365
+ */
366
+ setActive(credentialId: string): Promise<ProviderCredentialActive> | ProviderCredentialActive;
367
+ /**
368
+ * Run the vendor's public login protocol natively (device code or copy-back
369
+ * OAuth), surfacing user-facing material via `onPending`. On completion the
370
+ * material is imported into the pool and its `credentialId` is returned.
371
+ */
372
+ beginLogin?(options?: ProviderCredentialLoginOptions): Promise<ProviderCredentialLoginResult>;
373
+ /**
374
+ * Snapshot current vendor-default material into the demi pool.
375
+ * Assigns a stable `id` and returns public metadata (no secrets).
376
+ */
377
+ importDefault?(): Promise<ProviderCredentialInfo>;
378
+ /** Register material supplied by the product. */
379
+ add?(input: ProviderCredentialAddInput): Promise<ProviderCredentialInfo>;
380
+ /** Remove a pool entry. */
381
+ remove?(credentialId: string): Promise<void>;
382
+ }
252
383
  type ProviderRuntimeState = {
253
384
  status: 'unknown';
254
385
  message?: string;
@@ -268,6 +399,8 @@ interface Provider {
268
399
  auth?: ProviderAuth;
269
400
  /** Optional subscription / rate-limit quota surface (`@demicodes/provider` quota helpers). */
270
401
  quota?: ProviderQuota;
402
+ /** Optional multi-credential pool + global active switch. */
403
+ credentials?: ProviderCredentials;
271
404
  state?(): Promise<ProviderRuntimeState> | ProviderRuntimeState;
272
405
  listModels?(): Promise<ProviderModelList> | ProviderModelList;
273
406
  }
@@ -297,6 +430,10 @@ interface ProviderModel {
297
430
  outputLimit: number | null;
298
431
  supportsTools: boolean | null;
299
432
  supportsAttachments: boolean | null;
433
+ /** Whether the model accepts native video input (not frame extraction). Most models
434
+ * (all current Anthropic/Claude Code models) do not — their API has no video block.
435
+ * Optional: unset/undefined means "no video", so existing catalogs need no change. */
436
+ supportsVideo?: boolean | null;
300
437
  supportsReasoning: boolean | null;
301
438
  supportedThinkingEfforts: ThinkingEffort[] | null;
302
439
  defaultThinkingEffort: ThinkingEffort | null;
@@ -324,4 +461,4 @@ type ModelPolicy = {
324
461
  default?: string;
325
462
  };
326
463
  //#endregion
327
- export { ProviderQuotaSnapshot as A, usedPercentFromRatio as B, ProviderQuotaCapability as C, ProviderQuotaProbeOptions as D, ProviderQuotaProbeCost as E, clampUsedPercent as F, createProviderQuota as I, ensureQuota as L, ProviderQuotaUnsupportedError as M, ProviderQuotaWindow as N, ProviderQuotaProbeResult as O, ProviderQuotaWindowUnit as P, severityFromUsedPercent as R, ProviderQuota as S, ProviderQuotaPlan as T, ProviderSelection as _, ModelPolicy as a, CreateProviderQuotaOptions as b, ProviderAuthState as c, ProviderModel as d, ProviderModelCost as f, ProviderRuntimeState as g, ProviderRuntimeFactory as h, InferenceSteer as i, ProviderQuotaSource as j, ProviderQuotaSeverity as k, ProviderEvent as l, ProviderRun as m, InferenceItem as n, Provider as o, ProviderModelList as p, InferenceRequest as r, ProviderAuth as s, AgentProvider as t, ProviderFactoryDefinition as u, ProviderServiceTier as v, ProviderQuotaObserveInput as w, EnsureQuotaOptions as x, ToolDefinition as y, unixSecondsToIso as z };
464
+ export { ProviderQuota as A, ProviderQuotaUnsupportedError as B, ProviderRuntimeFactory as C, ToolDefinition as D, ProviderServiceTier as E, ProviderQuotaProbeOptions as F, ensureQuota as G, ProviderQuotaWindowUnit as H, ProviderQuotaProbeResult as I, usedPercentFromRatio as J, severityFromUsedPercent as K, ProviderQuotaSeverity as L, ProviderQuotaObserveInput as M, ProviderQuotaPlan as N, CreateProviderQuotaOptions as O, ProviderQuotaProbeCost as P, ProviderQuotaSnapshot as R, ProviderRun as S, ProviderSelection as T, clampUsedPercent as U, ProviderQuotaWindow as V, createProviderQuota as W, ProviderEvent as _, ModelPolicy as a, ProviderModelCost as b, ProviderAuthState as c, ProviderCredentialInfo as d, ProviderCredentialLoginOptions as f, ProviderCredentialsCapability as g, ProviderCredentials as h, InferenceSteer as i, ProviderQuotaCapability as j, EnsureQuotaOptions as k, ProviderCredentialActive as l, ProviderCredentialLoginResult as m, InferenceItem as n, Provider as o, ProviderCredentialLoginPending as p, unixSecondsToIso as q, InferenceRequest as r, ProviderAuth as s, AgentProvider as t, ProviderCredentialAddInput as u, ProviderFactoryDefinition as v, ProviderRuntimeState as w, ProviderModelList as x, ProviderModel as y, ProviderQuotaSource as z };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/provider",
3
3
  "description": "Provider contract and shared building blocks for Demi inference adapters.",
4
- "version": "0.10.3",
4
+ "version": "0.12.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -12,11 +12,15 @@
12
12
  "./testing": {
13
13
  "types": "./dist/testing.d.mts",
14
14
  "import": "./dist/testing.mjs"
15
+ },
16
+ "./credentials-pool": {
17
+ "types": "./dist/credentials-pool.d.mts",
18
+ "import": "./dist/credentials-pool.mjs"
15
19
  }
16
20
  },
17
21
  "dependencies": {
18
- "@demicodes/core": "^0.10.3",
19
- "@demicodes/utils": "^0.10.3"
22
+ "@demicodes/core": "^0.12.0",
23
+ "@demicodes/utils": "^0.12.0"
20
24
  },
21
25
  "license": "Apache-2.0",
22
26
  "main": "./dist/index.mjs",