@code-yeongyu/senpi-ai 2026.10.27-2 → 2026.10.30-2

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.
@@ -833,7 +833,7 @@ function consumeLine(text) {
833
833
  rest: text.slice(nextIndex),
834
834
  };
835
835
  }
836
- async function* iterateSseMessages(body, signal) {
836
+ async function* iterateSseMessages(body, signal, onBytes) {
837
837
  const reader = body.getReader();
838
838
  const decoder = new TextDecoder();
839
839
  const state = { event: null, data: [], raw: [] };
@@ -847,6 +847,8 @@ async function* iterateSseMessages(body, signal) {
847
847
  if (done) {
848
848
  break;
849
849
  }
850
+ if (value.byteLength > 0)
851
+ onBytes?.();
850
852
  buffer += decoder.decode(value, { stream: true });
851
853
  let consumed = consumeLine(buffer);
852
854
  while (consumed) {
@@ -883,13 +885,13 @@ async function* iterateSseMessages(body, signal) {
883
885
  reader.releaseLock();
884
886
  }
885
887
  }
886
- async function* iterateAnthropicEvents(response, signal) {
888
+ async function* iterateAnthropicEvents(response, signal, onTransportActivity) {
887
889
  if (!response.body) {
888
890
  throw new Error("Attempted to iterate over an Anthropic response with no body");
889
891
  }
890
892
  let sawMessageStart = false;
891
893
  let sawMessageEnd = false;
892
- for await (const sse of iterateSseMessages(response.body, signal)) {
894
+ for await (const sse of iterateSseMessages(response.body, signal, onTransportActivity)) {
893
895
  if (sse.event === "error") {
894
896
  let errorText = sse.data;
895
897
  const hintMs = extract429RetryAfterMs({ bodyText: sse.data });
@@ -1141,7 +1143,7 @@ export const stream = (model, context, options) => {
1141
1143
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
1142
1144
  stream.push({ type: "start", partial: output });
1143
1145
  const blocks = output.content;
1144
- for await (const event of iterateAnthropicEvents(response, requestSignal)) {
1146
+ for await (const event of iterateAnthropicEvents(response, requestSignal, options?.onTransportActivity)) {
1145
1147
  await options?.onProviderStreamEvent?.(event, model);
1146
1148
  if (event.type === "message_start") {
1147
1149
  output.responseId = event.message.id;
@@ -123,6 +123,7 @@ export function buildBaseOptions(model, context, options, apiKey) {
123
123
  onPayload: options?.onPayload,
124
124
  onResponse: options?.onResponse,
125
125
  onProviderStreamEvent: options?.onProviderStreamEvent,
126
+ onTransportActivity: options?.onTransportActivity,
126
127
  timeoutMs: options?.timeoutMs,
127
128
  websocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,
128
129
  maxRetries: options?.maxRetries,
@@ -36,6 +36,11 @@ export declare function applyAccountOrder<T extends {
36
36
  export declare function withoutOrderedAccount<T extends {
37
37
  order?: string[];
38
38
  }>(credential: T, name: string): T;
39
+ /**
40
+ * Whether selection skips a slot now: an auth block holds until a login rewrites the slot, whatever its deadline;
41
+ * any other block holds until its deadline. Account listings ask this too, so they never call a skipped slot available.
42
+ */
43
+ export declare function isSlotBlocked(slot: Pick<SelectableSlot, "blockedUntil" | "blockReason">, now: number): boolean;
39
44
  /** Elapsed rate/capacity blocks clear; auth blocks persist until a login rewrites the slot. */
40
45
  export declare function clearExpiredSlotBlocks(slots: readonly SelectableSlot[], now?: number): SelectableSlot[];
41
46
  /** Selects a pinned or HRW-ranked slot with no pool-global selection state. */
@@ -1,9 +1,17 @@
1
1
  export const DEFAULT_POOL_AFFINITY_KEY = "credential-pool-default";
2
2
  export class AllSlotsBlockedError extends Error {
3
3
  constructor(soonestUnblockAt) {
4
+ // Local time for the reader, the ISO instant for prose scanners, and one step (senpi#4329).
4
5
  super(soonestUnblockAt === undefined
5
- ? "All credential slots are blocked until re-login."
6
- : `All credential slots are blocked until ${new Date(soonestUnblockAt).toISOString()}.`);
6
+ ? "All credential slots are blocked until re-login. Sign in again or add a new API key for this provider, or choose another model."
7
+ : `All credential slots are blocked until ${new Date(soonestUnblockAt).toLocaleString("en-US", {
8
+ year: "numeric",
9
+ month: "short",
10
+ day: "numeric",
11
+ hour: "numeric",
12
+ minute: "2-digit",
13
+ timeZoneName: "short",
14
+ })} (${new Date(soonestUnblockAt).toISOString()}). Wait until then, or choose another model.`);
7
15
  this.name = "AllSlotsBlockedError";
8
16
  this.soonestUnblockAt = soonestUnblockAt;
9
17
  }
@@ -51,7 +59,11 @@ export function withoutOrderedAccount(credential, name) {
51
59
  next.order = order;
52
60
  return next;
53
61
  }
54
- function isBlocked(slot, now) {
62
+ /**
63
+ * Whether selection skips a slot now: an auth block holds until a login rewrites the slot, whatever its deadline;
64
+ * any other block holds until its deadline. Account listings ask this too, so they never call a skipped slot available.
65
+ */
66
+ export function isSlotBlocked(slot, now) {
55
67
  return slot.blockReason === "auth_error" || (slot.blockedUntil !== undefined && slot.blockedUntil > now);
56
68
  }
57
69
  /** Elapsed rate/capacity blocks clear; auth blocks persist until a login rewrites the slot. */
@@ -66,9 +78,9 @@ export function clearExpiredSlotBlocks(slots, now = Date.now()) {
66
78
  }
67
79
  function selectUnblocked(slots, options, now) {
68
80
  const pinned = options.pinnedSlot === undefined ? undefined : slots.find((slot) => slot.name === options.pinnedSlot);
69
- if (pinned && !isBlocked(pinned, now))
81
+ if (pinned && !isSlotBlocked(pinned, now))
70
82
  return pinned;
71
- return rendezvousOrder(getPoolAffinityKey(options), slots, options.hasher).find((slot) => !isBlocked(slot, now));
83
+ return rendezvousOrder(getPoolAffinityKey(options), slots, options.hasher).find((slot) => !isSlotBlocked(slot, now));
72
84
  }
73
85
  function soonestUnblockAt(slots, now) {
74
86
  const candidates = slots
@@ -11,6 +11,11 @@ export declare function isLegacyProviderId(id: string): boolean;
11
11
  * earlier version. Returns an empty array for an id that was never renamed.
12
12
  */
13
13
  export declare function legacyProviderIdsFor(canonicalId: string): string[];
14
+ /**
15
+ * The id upstream pi still uses for `providerId`: the legacy spelling of a renamed provider (the
16
+ * remote model catalog serves `openai-codex`, not `chatgpt-subscription`), the id itself otherwise.
17
+ */
18
+ export declare function upstreamProviderIdFor(providerId: string): string;
14
19
  /**
15
20
  * Read `record` by provider id, trying the canonical key first and then every
16
21
  * legacy spelling that normalizes to it. Never throws and never rewrites.
@@ -33,6 +33,13 @@ export function legacyProviderIdsFor(canonicalId) {
33
33
  .filter(([, canonical]) => canonical === canonicalId)
34
34
  .map(([legacy]) => legacy);
35
35
  }
36
+ /**
37
+ * The id upstream pi still uses for `providerId`: the legacy spelling of a renamed provider (the
38
+ * remote model catalog serves `openai-codex`, not `chatgpt-subscription`), the id itself otherwise.
39
+ */
40
+ export function upstreamProviderIdFor(providerId) {
41
+ return legacyProviderIdsFor(providerId)[0] ?? providerId;
42
+ }
36
43
  /**
37
44
  * Read `record` by provider id, trying the canonical key first and then every
38
45
  * legacy spelling that normalizes to it. Never throws and never rewrites.
package/dist/models.d.ts CHANGED
@@ -66,6 +66,11 @@ type ProviderModel<TApi extends Api> = Model<TApi> | ImageModel<ImageApi> | Clas
66
66
  export interface Provider<TApi extends Api = Api> {
67
67
  readonly id: string;
68
68
  readonly name: string;
69
+ /**
70
+ * The provider's endpoint. Transports send each request to `model.baseUrl`, never to this value or a
71
+ * hard-coded host: senpi sets every model's `baseUrl` (and this field) to a user's models.json
72
+ * `baseUrl` when one is configured for the provider.
73
+ */
69
74
  readonly baseUrl?: string;
70
75
  readonly headers?: ProviderHeaders;
71
76
  /** Omitting it means the shipped senpi default profile applies. */
@@ -1 +1 @@
1
- {"schemaVersion":6,"generatedAt":"2026-10-27T15:21:41.669Z","structureHash":"54d05f76e78487d58aa1a5f8f2756045ce929b90fbc75df55b07085a557ad2fa","files":{"alibaba-token-plan.json":"9f4d42cc765b2a875bcdcaa392bfdc1323c88968309baf9db9f611a26c595c73","amazon-bedrock.json":"88bc144ca751ec2b1cd90dc6c2bd79378f36f65e1f32b3d5c49d8273a97bb5e1","ant-ling.json":"628feab3ef6c7d80ce96f75999804e1d9f05fec3851e5302e30fff3b69db90f6","anthropic.json":"111384e9f5845e0247bd34edf2400ef7720fcbdac140b944cf8878192d8bd6ab","azure-openai-responses.json":"fd64c6a2a99fa87d768ed2f75f5c7813876df1dbe2f0d1776f7d9bd7ce2e61e4","bai.json":"7e6861aaf27d8af34e1771c6092602194fcec0a0046e6adfab9c3ee54d32b99f","baseten.json":"5bd3c98c61769388b3724368ef964d16b7a58e41a6edcf4ebceb9d4ac0a21d3b","cerebras.json":"2eb969b2956844f9a641d3de52c19f1791293c58f607282fb3b0ebfe85ff66f8","chatgpt-subscription.json":"42b88c93d717b2a7469b71da7da5adf87347d0d9cc0f264b39117f0f940b51c0","cloudflare-ai-gateway.json":"4d821d1d0d6873240a6a7f4ba9feaade4f182ceaa0b1493f38e4973c4d05f375","cloudflare-workers-ai.json":"5e4d71ddbb66e903d98076f6de66e5627f19621e969f822a505e9d13eaf729ff","deepseek.json":"10a296fb3e898f7715c80890c8af0af4f5fd58f22dbcd6612fdc5d762157ccdc","fireworks.json":"78994dee5b97c276906ae6d182329871f2ffa86da7cf8470785b6110e1c50218","github-copilot.json":"474fa92eb88ef242208ca93709ebe6d7f21754917d0cad6210bd1edf2b99a87e","google-vertex.json":"8f986b4aca8c685b0f3481bd252bda21e4858bf9015ad37fc5bcc44b7f01d076","google.json":"d25613a7377e81d785d571be514d67b35436511ce6946247dc66f8674e2a5035","groq.json":"087df37206e9eed8b10f1e558e4c496289bef60074b7e8afba4f20fb102b4ec2","huggingface.json":"209cdcf205961ef2ff203486da614232a86a069ebe0e98266189a14a235fac8f","meta.json":"563f5ec07973f52ab8c3eb26ef91a36ad8a79e950b37174935666cdcb624bf35","minimax-cn.json":"70df757e95e9ea2275bf451956b58a746020835f6cbcbd1a644d7fad5b9941d3","minimax.json":"747038b05f4fc24e41405e8bc810f893d7d5121e214e35e6f812f775769a3806","mistral.json":"8b6dbb21d6794d3ef606147ac7b05c355748a640224da9072f1081a908fe444f","moonshotai-cn.json":"a953eace729ef96c91195d477c63c776a1c40aa36085b61236ca1be0fe5b9337","moonshotai.json":"0435f9dc4e532c0764b6d33d8dfa83ce5fccdbcab54224c5ae74ec2d965290f5","nvidia.json":"6f66192c21788b4555f7e77bd7c14201172edf355be8d318f96ff8d0d6921054","openai.json":"d61200cdd22d931dfff8181a5ed8d636c89fb34637dcec80f07ec24f8c2ef5c6","opencode-go.json":"2a5e0424854edfc0ef9a49033e80a8eb32c01a98c56ba2879b36a9502baa4027","opencode.json":"2d210be6553e367e6fd420ac278e57536d7a0752a37a91984a7511fba9b5ac0e","opengateway.json":"18e0ed43f20b28430c412fc9ed1f27678a77bed7474d9c498abff82f85c3d494","openrouter.json":"26a1552fa113091abf9ae005ab96f4c02131058a652b98d3e8b642acea8847da","qwen-token-plan-cn.json":"751b18bd443fceb16659ea33013fed90fbf3bbdb4e0ffd7607c93cee2b4cf40d","qwen-token-plan-individual.json":"35ffb0ba1a1ce3f68fc73262dd5e07fbcb5bf5911ad1e6e17bc0621e2894676a","qwen-token-plan.json":"e59b0a45e238578f80d0eea937a093b9cec7cb93751ae20a9e88b16c09eb5cdd","radius.json":"bf4bfe986dc3701d478db53b9cf4e1facc8fa7a008e4a0c9e77fc1f5c2692bc0","together.json":"a68b8386a2f28f9e1570d296223fdbdc0ebcca8a322ade5b999212c0e60b772c","typesafe.json":"83409437dbf4c353ac413c0db4e7cf29902fc35e0e0197bdb4eeda2d7f3b07cd","venice.json":"dee127f9fa48768701ee4f59f1c318e55a4b1f25d6bac38ea6f105eddcd60889","vercel-ai-gateway.json":"bac648af7c609376e596571bad598a848dd93d04448662fc04fcac49d3efc9b5","xai.json":"cf3f475fe5a13625f858171900ba3456c4d95ebb0bac5060cc25e29bb31d3457","xiaomi-token-plan-ams.json":"7fa339ba5675460b1ff4e5bca9e8ad0aa60b0277d75a989b1c196aa6ca2af34d","xiaomi-token-plan-cn.json":"ef7917c6ea0d3edc655c8a52082ee5f1330658aa6f10199d8f164eb4605ef0a0","xiaomi-token-plan-sgp.json":"addda93f6b70ba879db97e3cfd2e4f2efc7cda47d7daa78c23c5b0e51162de5c","xiaomi.json":"5a42caf49d3e4d7946d60386e9899c2146bf8e3cbaff4c576ee4f49b398cc11c","zai-coding-cn.json":"2b6f85e04b5c568e320b1bde25b1c1614af9342e4c1ba0e1d5bb248aaf50cfe9","zai.json":"e9cc408e1756de8abc70148d63d89d11a63c65b7ef06ad0661cd1328e4209ba3"}}
1
+ {"schemaVersion":6,"generatedAt":"2026-10-30T18:20:52.997Z","structureHash":"536b4b653e1cd2b06b71fd0a91cc07d21a2e207657c728557c82e512ac304180","files":{"alibaba-token-plan.json":"9f4d42cc765b2a875bcdcaa392bfdc1323c88968309baf9db9f611a26c595c73","amazon-bedrock.json":"a7e20838b095788cb9d5acbde49664b7f2868fad9b10fb4bbbd66635085bca2d","ant-ling.json":"628feab3ef6c7d80ce96f75999804e1d9f05fec3851e5302e30fff3b69db90f6","anthropic.json":"111384e9f5845e0247bd34edf2400ef7720fcbdac140b944cf8878192d8bd6ab","azure-openai-responses.json":"fd64c6a2a99fa87d768ed2f75f5c7813876df1dbe2f0d1776f7d9bd7ce2e61e4","bai.json":"7e6861aaf27d8af34e1771c6092602194fcec0a0046e6adfab9c3ee54d32b99f","baseten.json":"5bd3c98c61769388b3724368ef964d16b7a58e41a6edcf4ebceb9d4ac0a21d3b","cerebras.json":"2eb969b2956844f9a641d3de52c19f1791293c58f607282fb3b0ebfe85ff66f8","chatgpt-subscription.json":"98320f1d51709d26ca047b3d6e3191cfe2f192f1b27897cbb492f47b0e5a5030","cloudflare-ai-gateway.json":"1faa40d07413f2dd8a4c387dcae2c8d084e222d3191730f5bd4af48b2fa6ba34","cloudflare-workers-ai.json":"1b9ad4360bde817f0ea358431020308f1052840e2278a57a93629f1790072d2e","deepseek.json":"10a296fb3e898f7715c80890c8af0af4f5fd58f22dbcd6612fdc5d762157ccdc","fireworks.json":"56c557eb97573028e871c211cb17e1d60665eb12d67a712615db4bb255bc9253","github-copilot.json":"474fa92eb88ef242208ca93709ebe6d7f21754917d0cad6210bd1edf2b99a87e","google-vertex.json":"8f986b4aca8c685b0f3481bd252bda21e4858bf9015ad37fc5bcc44b7f01d076","google.json":"d25613a7377e81d785d571be514d67b35436511ce6946247dc66f8674e2a5035","groq.json":"087df37206e9eed8b10f1e558e4c496289bef60074b7e8afba4f20fb102b4ec2","huggingface.json":"209cdcf205961ef2ff203486da614232a86a069ebe0e98266189a14a235fac8f","meta.json":"563f5ec07973f52ab8c3eb26ef91a36ad8a79e950b37174935666cdcb624bf35","minimax-cn.json":"70df757e95e9ea2275bf451956b58a746020835f6cbcbd1a644d7fad5b9941d3","minimax.json":"747038b05f4fc24e41405e8bc810f893d7d5121e214e35e6f812f775769a3806","mistral.json":"aafecc66686645b1baa8c90e81c02856115f09999ff507f9bee004d2e1c77a28","moonshotai-cn.json":"a953eace729ef96c91195d477c63c776a1c40aa36085b61236ca1be0fe5b9337","moonshotai.json":"0435f9dc4e532c0764b6d33d8dfa83ce5fccdbcab54224c5ae74ec2d965290f5","nvidia.json":"0e45bcd4d5a897f6f6785c0e257a9fb03691cef3ff87d9254da5daa5075468da","openai.json":"d61200cdd22d931dfff8181a5ed8d636c89fb34637dcec80f07ec24f8c2ef5c6","opencode-go.json":"2a5e0424854edfc0ef9a49033e80a8eb32c01a98c56ba2879b36a9502baa4027","opencode.json":"0e08d3fc23284852968106c4e0c17e85adedf729db145b8c7871e9bd999845b1","opengateway.json":"92262b093ece3a900faccc547e5dc19688548e7e41c360ddb657b2e9deb36e98","openrouter.json":"0115939af894fec1b173573c9a9d7f22a643ec3a2063d5515973cb873ed2c01b","qwen-token-plan-cn.json":"751b18bd443fceb16659ea33013fed90fbf3bbdb4e0ffd7607c93cee2b4cf40d","qwen-token-plan-individual.json":"35ffb0ba1a1ce3f68fc73262dd5e07fbcb5bf5911ad1e6e17bc0621e2894676a","qwen-token-plan.json":"e59b0a45e238578f80d0eea937a093b9cec7cb93751ae20a9e88b16c09eb5cdd","radius.json":"5241be199f873fd7474b1ccb1fc911af223903af681909029fefe9fb8b49f83c","together.json":"a68b8386a2f28f9e1570d296223fdbdc0ebcca8a322ade5b999212c0e60b772c","typesafe.json":"83409437dbf4c353ac413c0db4e7cf29902fc35e0e0197bdb4eeda2d7f3b07cd","venice.json":"b835ee2e486477dc306d8e82accc06fe8a4bb708471664530e5681811174072d","vercel-ai-gateway.json":"7ae8df20eff08fa919f5aa3b04298fe1eb4614f6a3e62f64000645cab2120279","xai.json":"cf3f475fe5a13625f858171900ba3456c4d95ebb0bac5060cc25e29bb31d3457","xiaomi-token-plan-ams.json":"7fa339ba5675460b1ff4e5bca9e8ad0aa60b0277d75a989b1c196aa6ca2af34d","xiaomi-token-plan-cn.json":"ef7917c6ea0d3edc655c8a52082ee5f1330658aa6f10199d8f164eb4605ef0a0","xiaomi-token-plan-sgp.json":"addda93f6b70ba879db97e3cfd2e4f2efc7cda47d7daa78c23c5b0e51162de5c","xiaomi.json":"5a42caf49d3e4d7946d60386e9899c2146bf8e3cbaff4c576ee4f49b398cc11c","zai-coding-cn.json":"2b6f85e04b5c568e320b1bde25b1c1614af9342e4c1ba0e1d5bb248aaf50cfe9","zai.json":"e9cc408e1756de8abc70148d63d89d11a63c65b7ef06ad0661cd1328e4209ba3"}}