@consultancy-outfit/ai-draftsman-cli 1.1.1 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.js +46 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -719,6 +719,19 @@ var VALID_TOOL_NAME = /^[a-zA-Z0-9_.-]+$/;
719
719
  var INVALID_TOOL_NAME = "invalid_tool";
720
720
  var MAX_RETRIES = 4;
721
721
  var MAX_WAIT_SECONDS = 90;
722
+ var UPSTREAM_BASE_WAIT_SECONDS = 5;
723
+ var UPSTREAM_RATE_LIMIT = /RESOURCE_EXHAUSTED|RATE_LIMIT|\b429\b|quota exceeded|too many requests/i;
724
+ var UpstreamRateLimit = class extends Error {
725
+ };
726
+ function isUpstreamRateLimit(message, code) {
727
+ return /^(resource_exhausted|rate_limit(_exceeded)?)$/i.test(code ?? "") || UPSTREAM_RATE_LIMIT.test(message);
728
+ }
729
+ function upstreamRateLimitError(message) {
730
+ return new CliError(
731
+ `The model provider is rate limiting requests (${message.replace(/\s+/g, " ").trim()}). Retries did not get through. Wait a minute and try again, or pick another model with /agents.`,
732
+ 6
733
+ );
734
+ }
722
735
  function retryDelaySeconds(response, body, attempt) {
723
736
  const header = Number(response.headers.get("retry-after"));
724
737
  if (Number.isFinite(header) && header > 0) return header;
@@ -760,26 +773,40 @@ async function streamChat(config, request, options = {}) {
760
773
  stream_options: { include_usage: true },
761
774
  ...request.tools?.length ? { tools: request.tools, tool_choice: "auto" } : {}
762
775
  });
763
- let response;
776
+ const waitOut = async (seconds, who) => {
777
+ const pause = options.sleep ?? sleep;
778
+ for (let left = seconds; left > 0; left--) {
779
+ options.onStatus?.(`Rate limited by ${who} \xB7 retrying in ${left}s (esc to cancel)`);
780
+ await pause(1e3, options.signal);
781
+ }
782
+ options.onStatus?.(void 0);
783
+ };
784
+ let upstreamRetries = 0;
764
785
  for (let attempt = 0; ; attempt++) {
765
- response = await fetch(new URL("/v1/chat/completions", config.baseUrl), {
786
+ const response = await fetch(new URL("/v1/chat/completions", config.baseUrl), {
766
787
  method: "POST",
767
788
  headers,
768
789
  body,
769
790
  signal: options.signal
770
791
  });
771
- if (response.status !== 429) break;
772
- const text2 = await response.text().catch(() => "");
773
- const wait = retryDelaySeconds(response, text2, attempt);
774
- if (attempt >= MAX_RETRIES || wait > MAX_WAIT_SECONDS) throw rateLimitError(text2, wait);
775
- const pause = options.sleep ?? sleep;
776
- for (let left = wait; left > 0; left--) {
777
- options.onStatus?.(`Rate limited by the gateway \xB7 retrying in ${left}s (esc to cancel)`);
778
- await pause(1e3, options.signal);
792
+ if (response.status === 429) {
793
+ const text2 = await response.text().catch(() => "");
794
+ const wait = retryDelaySeconds(response, text2, attempt);
795
+ if (attempt >= MAX_RETRIES || wait > MAX_WAIT_SECONDS) throw rateLimitError(text2, wait);
796
+ await waitOut(wait, "the gateway");
797
+ continue;
798
+ }
799
+ try {
800
+ if (!response.ok) throw await httpError(response);
801
+ return await readCompletion(response, options);
802
+ } catch (error) {
803
+ if (!(error instanceof UpstreamRateLimit)) throw error;
804
+ if (upstreamRetries >= MAX_RETRIES) throw upstreamRateLimitError(error.message);
805
+ await waitOut(UPSTREAM_BASE_WAIT_SECONDS * 2 ** upstreamRetries++, "the model provider");
779
806
  }
780
- options.onStatus?.(void 0);
781
807
  }
782
- if (!response.ok) throw await httpError(response);
808
+ }
809
+ async function readCompletion(response, options) {
783
810
  if (!response.body) throw new CliError("The server sent an empty response.", 8);
784
811
  let content = "";
785
812
  let finishReason = "stop";
@@ -798,7 +825,12 @@ async function streamChat(config, request, options = {}) {
798
825
  continue;
799
826
  }
800
827
  if (chunk.error) {
801
- throw new CliError(chunk.error.message ?? "The model request failed.", chunk.error.code === "resource_exhausted" ? 6 : 8);
828
+ const message = chunk.error.message ?? "The model request failed.";
829
+ if (isUpstreamRateLimit(message, chunk.error.code)) {
830
+ if (!content && calls.size === 0) throw new UpstreamRateLimit(message);
831
+ throw upstreamRateLimitError(message);
832
+ }
833
+ throw new CliError(message, 8);
802
834
  }
803
835
  if (chunk.usage) usage = chunk.usage;
804
836
  const choice2 = chunk.choices?.[0];
@@ -865,6 +897,7 @@ async function httpError(response) {
865
897
  } catch {
866
898
  }
867
899
  const detail = message || response.statusText;
900
+ if (response.status !== 401 && response.status !== 402 && /RESOURCE_EXHAUSTED/i.test(text2)) return new UpstreamRateLimit(detail);
868
901
  if (response.status === 401) return new CliError("Your API key was rejected. Run /login to sign in again.", 2);
869
902
  if (response.status === 402) return new CliError(`Spend cap reached: ${detail}`, 7);
870
903
  if (response.status === 403) return new CliError(`Not allowed: ${detail}`, 4);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consultancy-outfit/ai-draftsman-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "AI Draftsman standalone CLI",
5
5
  "type": "module",
6
6
  "files": [